OLD | NEW |
(Empty) | |
| 1 (function(scope) { |
| 2 "use strict"; |
| 3 function getStickyElements(nodelist, node) { |
| 4 nodelist = nodelist || []; |
| 5 node = node || document.body; |
| 6 if (node.classList.contains('sticky')) |
| 7 nodelist.push(node); |
| 8 // Q: How do we support this position property? |
| 9 // if (getComputedStyle(node).position == 'sticky') |
| 10 // nodelist.append(node); |
| 11 |
| 12 // Q: How do we just do work on sticky elements rather than entire DOM? |
| 13 for (var i = 0; i < node.children.length; ++i) { |
| 14 getStickyElements(nodelist, node.children[i]); |
| 15 } |
| 16 return nodelist; |
| 17 } |
| 18 |
| 19 function installStickyHandler(scroller, element, sticky_info) { |
| 20 var main_info = { |
| 21 'sticky': new WrapElement(element), |
| 22 'scroller': new WrapElement(scroller), |
| 23 'limits': sticky_info.limits, |
| 24 }; |
| 25 |
| 26 // Install a viewport observer to update position on the main thread. |
| 27 var observer = new ViewportObserver(updateStickyPosition.bind(null, main_inf
o)); |
| 28 observer.observe(scroller); |
| 29 // TODO: Install compositor worker to update on compositor thread. |
| 30 } |
| 31 |
| 32 function updateStickyPosition(info) { |
| 33 // Find sticky position |
| 34 // Note: top and left take priority over bottom and right as per spec: |
| 35 var offsetX, offsetY; |
| 36 if (info.limits.y.attachment == 'top') |
| 37 offsetY = Math.min(Math.max(info.limits.y.min, info.scroller.scrollTop() -
info.limits.y.top), info.limits.y.max); |
| 38 if (info.limits.y.attachment == 'bottom') |
| 39 offsetY = Math.max(Math.min(info.limits.y.max, info.scroller.scrollTop() +
info.limits.y.bottom), info.limits.y.min); |
| 40 if (info.limits.x.attachment == 'left') |
| 41 offsetX = Math.min(Math.max(info.limits.x.min, info.scroller.scrollLeft()
- info.limits.x.left), info.limits.x.max); |
| 42 if (info.limits.x.attachment == 'right') |
| 43 offsetX = Math.max(Math.min(info.limits.x.max, info.scroller.scrollLeft()
+ info.limits.x.right), info.limits.x.min); |
| 44 |
| 45 // Q: How should we really move the element around? This is overloading the
top / left properties which has a specific meaning for sticky. |
| 46 // A: Probably hidden position attributes (e.g. used left, used top), but ho
w do we want to expose these? |
| 47 info.sticky.setTransform(offsetX || 0, offsetY || 0, info.limits.id); |
| 48 } |
| 49 |
| 50 function gatherStickyInfo() { |
| 51 var nodes = getStickyElements(); |
| 52 scope.sticky_info = []; |
| 53 for (var i = 0; i < nodes.length; i++) { |
| 54 var node = nodes[i]; |
| 55 var cb = getContainingBlockElement(node); |
| 56 var s = getContainingScrollingElement(node); |
| 57 |
| 58 // Q: Need to be notified when sticky node is removed / moved - Mutation O
bserver? |
| 59 var info = { |
| 60 sticky: scope.CompositorProxy ? new CompositorProxy(node, ['transform'])
: undefined, |
| 61 scroller: scope.CompositorProxy ? new CompositorProxy(s, ['scrollTop', '
scrollLeft']) : undefined, |
| 62 limits: scope.getStickyOffsetLimits(node, cb, s, getStickyOffsets(node))
, |
| 63 } |
| 64 scope.sticky_info.push(info); |
| 65 installStickyHandler(s, node, info); |
| 66 } |
| 67 if (scope.CompositorWorker) { |
| 68 scope.worker = new CompositorWorker('js/sticky.js'); |
| 69 scope.worker.postMessage(scope.sticky_info); |
| 70 } |
| 71 } |
| 72 |
| 73 scope.getStickyOffsetLimits = function(sticky, containing, scroller, clamp) { |
| 74 var c = containing.getBoundingClientRect(); |
| 75 var s = sticky.getBoundingClientRect(); |
| 76 var v = getViewportRect(scroller); |
| 77 |
| 78 var limits = { |
| 79 id: sticky.id, |
| 80 x: { |
| 81 left: s.left + scroller.scrollLeft - (clamp.left || 0), |
| 82 right: v.right - s.right - scroller.scrollLeft - (clamp.right || 0), |
| 83 min: c.left - s.left + (clamp.left || 0), |
| 84 max: c.right - s.right - (clamp.right || 0), |
| 85 }, |
| 86 y: { |
| 87 top: s.top + scroller.scrollTop - (clamp.top || 0), |
| 88 bottom: v.bottom - s.bottom - scroller.scrollTop - (clamp.bottom || 0), |
| 89 min: c.top - s.top + (clamp.top || 0), |
| 90 max: c.bottom - s.bottom - (clamp.bottom || 0), |
| 91 }}; |
| 92 if (clamp.bottom) |
| 93 limits.y.attachment = 'bottom'; |
| 94 if (clamp.top) |
| 95 limits.y.attachment = 'top'; |
| 96 if (clamp.right) |
| 97 limits.x.attachment = 'right'; |
| 98 if (clamp.left) |
| 99 limits.x.attachment = 'left'; |
| 100 return limits; |
| 101 } |
| 102 |
| 103 function getStickyOffsets(node) { |
| 104 var style = getComputedStyle(node); |
| 105 // Note: this assumes top/left/right/bottom will be set in 'px'. |
| 106 // TODO: Support percentage? |
| 107 var info = { |
| 108 'top': style.top == 'auto' ? undefined : parseFloat(style.top), |
| 109 'left': style.left == 'auto' ? undefined : parseFloat(style.left), |
| 110 'right': style.right == 'auto' ? undefined : parseFloat(style.right), |
| 111 'bottom': style.bottom == 'auto' ? undefined : parseFloat(style.bottom), |
| 112 }; |
| 113 node.style.top = node.style.left = node.style.right = node.style.bottom = 'a
uto'; |
| 114 return info; |
| 115 } |
| 116 |
| 117 scope.compositorUpdate = function() { |
| 118 for (var i = 0; i < scope.sticky_info.length; i++) { |
| 119 updateStickyPosition(scope.sticky_info[i]); |
| 120 } |
| 121 scope.requestAnimationFrame(scope.compositorUpdate); |
| 122 } |
| 123 |
| 124 scope.WrapElement = function(element) { |
| 125 this.element_ = element; |
| 126 } |
| 127 scope.WrapElement.prototype.scrollTop = function() { return this.element_.scro
llTop; }; |
| 128 scope.WrapElement.prototype.scrollLeft = function() { return this.element_.scr
ollLeft; }; |
| 129 scope.WrapElement.prototype.setTransform = function(x, y, id, extra) { |
| 130 this.element_.style.transform = 'translate(' + x + 'px, ' + y + 'px)'; |
| 131 }; |
| 132 |
| 133 scope.WrapCompositorProxy = function(proxy) { |
| 134 this.proxy_ = proxy; |
| 135 } |
| 136 scope.WrapCompositorProxy.prototype.scrollTop = function() { return this.proxy
_.scrollTop; }; |
| 137 scope.WrapCompositorProxy.prototype.scrollLeft = function() { return this.prox
y_.scrollLeft; }; |
| 138 scope.WrapCompositorProxy.prototype.setTransform = function(x, y, id, extra) { |
| 139 var t = this.proxy_.transform; |
| 140 t.m41 = x; |
| 141 t.m42 = y; |
| 142 this.proxy_.transform = t; |
| 143 }; |
| 144 |
| 145 |
| 146 function initCompositorWorker() { |
| 147 console.log('Init on compoositor worker'); |
| 148 self.onmessage = function(e) { |
| 149 scope.sticky_info = e.data; |
| 150 // Wrap all of the compositor proxies to match expectations. |
| 151 for (var i = 0; i < sticky_info.length; i++) { |
| 152 sticky_info[i].sticky = new scope.WrapCompositorProxy(sticky_info[i].sti
cky); |
| 153 sticky_info[i].scroller = new scope.WrapCompositorProxy(sticky_info[i].s
croller); |
| 154 } |
| 155 scope.requestAnimationFrame(scope.compositorUpdate); |
| 156 console.log('onmessage on compositor'); |
| 157 }; |
| 158 } |
| 159 |
| 160 |
| 161 if (scope.window) { |
| 162 scope.document.addEventListener('DOMContentLoaded', gatherStickyInfo); |
| 163 } else { |
| 164 scope.sticky_info = []; |
| 165 initCompositorWorker(); |
| 166 } |
| 167 })(self); |
OLD | NEW |