| OLD | NEW |
| (Empty) | |
| 1 |
| 2 |
| 3 Polymer.DomApi = (function() { |
| 4 |
| 5 var Debounce = Polymer.Debounce; |
| 6 var Settings = Polymer.Settings; |
| 7 |
| 8 var nativeInsertBefore = Element.prototype.insertBefore; |
| 9 var nativeRemoveChild = Element.prototype.removeChild; |
| 10 var nativeAppendChild = Element.prototype.appendChild; |
| 11 |
| 12 var dirtyRoots = []; |
| 13 |
| 14 var DomApi = function(node, patch) { |
| 15 this.node = node; |
| 16 if (patch) { |
| 17 this.patch(); |
| 18 } |
| 19 }; |
| 20 |
| 21 DomApi.prototype = { |
| 22 |
| 23 // experimental: support patching selected native api. |
| 24 patch: function() { |
| 25 var self = this; |
| 26 this.node.appendChild = function(node) { |
| 27 return self.appendChild(node); |
| 28 }; |
| 29 this.node.insertBefore = function(node, ref_node) { |
| 30 return self.insertBefore(node, ref_node); |
| 31 }; |
| 32 this.node.removeChild = function(node) { |
| 33 return self.removeChild(node); |
| 34 }; |
| 35 }, |
| 36 |
| 37 get childNodes() { |
| 38 var c$ = getLightChildren(this.node); |
| 39 return Array.isArray(c$) ? c$ : Array.prototype.slice.call(c$); |
| 40 }, |
| 41 |
| 42 get children() { |
| 43 return Array.prototype.filter.call(this.childNodes, function(n) { |
| 44 return (n.nodeType === Node.ELEMENT_NODE); |
| 45 }); |
| 46 }, |
| 47 |
| 48 get parentNode() { |
| 49 return this.node.lightParent || this.node.parentNode; |
| 50 }, |
| 51 |
| 52 flush: function() { |
| 53 for (var i=0, host; i<dirtyRoots.length; i++) { |
| 54 host = dirtyRoots[i]; |
| 55 host.flushDebouncer('_distribute'); |
| 56 } |
| 57 dirtyRoots = []; |
| 58 }, |
| 59 |
| 60 _lazyDistribute: function(host) { |
| 61 if (host.shadyRoot) { |
| 62 host.shadyRoot._distributionClean = false; |
| 63 } |
| 64 // TODO(sorvell): optimize debounce so it does less work by default |
| 65 // and then remove these checks... |
| 66 // need to dirty distribution once. |
| 67 if (!host.isDebouncerActive('_distribute')) { |
| 68 host.debounce('_distribute', host._distributeContent); |
| 69 dirtyRoots.push(host); |
| 70 } |
| 71 }, |
| 72 |
| 73 // cases in which we may not be able to just do standard appendChild |
| 74 // 1. container has a shadyRoot (needsDistribution IFF the shadyRoot |
| 75 // has an insertion point) |
| 76 // 2. container is a shadyRoot (don't distribute, instead set |
| 77 // container to container.host. |
| 78 // 3. node is <content> (host of container needs distribution) |
| 79 appendChild: function(node) { |
| 80 var distributed; |
| 81 this._removeNodeFromHost(node); |
| 82 if (this._nodeIsInLogicalTree(this.node)) { |
| 83 var host = this._hostForNode(this.node); |
| 84 this._addLogicalInfo(node, this.node, host && host.shadyRoot); |
| 85 this._addNodeToHost(node); |
| 86 if (host) { |
| 87 distributed = this._maybeDistribute(node, this.node, host); |
| 88 } |
| 89 } |
| 90 if (!distributed) { |
| 91 // if adding to a shadyRoot, add to host instead |
| 92 var container = this.node._isShadyRoot ? this.node.host : this.node; |
| 93 nativeAppendChild.call(container, node); |
| 94 } |
| 95 return node; |
| 96 }, |
| 97 |
| 98 insertBefore: function(node, ref_node) { |
| 99 if (!ref_node) { |
| 100 return this.appendChild(node); |
| 101 } |
| 102 var distributed; |
| 103 this._removeNodeFromHost(node); |
| 104 if (this._nodeIsInLogicalTree(this.node)) { |
| 105 saveLightChildrenIfNeeded(this.node); |
| 106 var children = this.childNodes; |
| 107 var index = children.indexOf(ref_node); |
| 108 if (index < 0) { |
| 109 throw Error('The ref_node to be inserted before is not a child ' + |
| 110 'of this node'); |
| 111 } |
| 112 var host = this._hostForNode(this.node); |
| 113 this._addLogicalInfo(node, this.node, host && host.shadyRoot, index); |
| 114 this._addNodeToHost(node); |
| 115 if (host) { |
| 116 distributed = this._maybeDistribute(node, this.node, host); |
| 117 } |
| 118 } |
| 119 if (!distributed) { |
| 120 // if ref_node is <content> replace with first distributed node |
| 121 ref_node = ref_node.localName === CONTENT ? |
| 122 this._firstComposedNode(ref_node) : ref_node; |
| 123 // if adding to a shadyRoot, add to host instead |
| 124 var container = this.node._isShadyRoot ? this.node.host : this.node; |
| 125 nativeInsertBefore.call(container, node, ref_node); |
| 126 } |
| 127 return node; |
| 128 }, |
| 129 |
| 130 /** |
| 131 Removes the given `node` from the element's `lightChildren`. |
| 132 This method also performs dom composition. |
| 133 */ |
| 134 removeChild: function(node) { |
| 135 var distributed; |
| 136 if (this._nodeIsInLogicalTree(this.node)) { |
| 137 var host = this._hostForNode(this.node); |
| 138 distributed = this._maybeDistribute(node, this.node, host); |
| 139 this._removeNodeFromHost(node); |
| 140 } |
| 141 if (!distributed) { |
| 142 // if removing from a shadyRoot, remove form host instead |
| 143 var container = this.node._isShadyRoot ? this.node.host : this.node; |
| 144 nativeRemoveChild.call(container, node); |
| 145 } |
| 146 return node; |
| 147 }, |
| 148 |
| 149 replaceChild: function(node, ref_node) { |
| 150 this.insertBefore(node, ref_node); |
| 151 this.removeChild(ref_node); |
| 152 return node; |
| 153 }, |
| 154 |
| 155 getOwnerRoot: function() { |
| 156 return this._ownerShadyRootForNode(this.node); |
| 157 }, |
| 158 |
| 159 _ownerShadyRootForNode: function(node) { |
| 160 if (node._ownerShadyRoot === undefined) { |
| 161 var root; |
| 162 if (node._isShadyRoot) { |
| 163 root = node; |
| 164 } else { |
| 165 var parent = Polymer.dom(node).parentNode; |
| 166 if (parent) { |
| 167 root = parent._isShadyRoot ? parent : |
| 168 this._ownerShadyRootForNode(parent); |
| 169 } else { |
| 170 root = null; |
| 171 } |
| 172 } |
| 173 node._ownerShadyRoot = root; |
| 174 } |
| 175 return node._ownerShadyRoot; |
| 176 |
| 177 }, |
| 178 |
| 179 _maybeDistribute: function(node, parent, host) { |
| 180 var nodeNeedsDistribute = this._nodeNeedsDistribution(node); |
| 181 var distribute = this._parentNeedsDistribution(parent) || |
| 182 nodeNeedsDistribute; |
| 183 if (nodeNeedsDistribute) { |
| 184 this._updateInsertionPoints(host); |
| 185 } |
| 186 if (distribute) { |
| 187 this._lazyDistribute(host); |
| 188 } |
| 189 return distribute; |
| 190 }, |
| 191 |
| 192 _updateInsertionPoints: function(host) { |
| 193 host.shadyRoot._insertionPoints = |
| 194 factory(host.shadyRoot).querySelectorAll(CONTENT); |
| 195 }, |
| 196 |
| 197 _nodeIsInLogicalTree: function(node) { |
| 198 return Boolean(node._isShadyRoot || |
| 199 this._ownerShadyRootForNode(node) || |
| 200 node.shadyRoot); |
| 201 }, |
| 202 |
| 203 // note: a node is its own host |
| 204 _hostForNode: function(node) { |
| 205 var root = node.shadyRoot || (node._isShadyRoot ? |
| 206 node : this._ownerShadyRootForNode(node)); |
| 207 return root && root.host; |
| 208 }, |
| 209 |
| 210 _parentNeedsDistribution: function(parent) { |
| 211 return parent.shadyRoot && hasInsertionPoint(parent.shadyRoot); |
| 212 }, |
| 213 |
| 214 // TODO(sorvell): technically we should check non-fragment nodes for |
| 215 // <content> children but since this case is assumed to be exceedingly |
| 216 // rare, we avoid the cost and will address with some specific api |
| 217 // when the need arises. |
| 218 _nodeNeedsDistribution: function(node) { |
| 219 return (node.localName === CONTENT) || |
| 220 ((node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) && |
| 221 node.querySelector(CONTENT)); |
| 222 }, |
| 223 |
| 224 _removeNodeFromHost: function(node) { |
| 225 if (node.lightParent) { |
| 226 var root = this._ownerShadyRootForNode(node); |
| 227 if (root) { |
| 228 root.host._elementRemove(node); |
| 229 } |
| 230 this._removeLogicalInfo(node, node.lightParent); |
| 231 } |
| 232 this._removeOwnerShadyRoot(node); |
| 233 }, |
| 234 |
| 235 _addNodeToHost: function(node) { |
| 236 var checkNode = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? |
| 237 node.firstChild : node; |
| 238 var root = this._ownerShadyRootForNode(checkNode); |
| 239 if (root) { |
| 240 root.host._elementAdd(node); |
| 241 } |
| 242 }, |
| 243 |
| 244 _addLogicalInfo: function(node, container, root, index) { |
| 245 saveLightChildrenIfNeeded(container); |
| 246 var children = factory(container).childNodes; |
| 247 index = index === undefined ? children.length : index; |
| 248 // handle document fragments |
| 249 if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 250 var n = node.firstChild; |
| 251 while (n) { |
| 252 children.splice(index++, 0, n); |
| 253 n.lightParent = container; |
| 254 n = n.nextSibling; |
| 255 } |
| 256 } else { |
| 257 children.splice(index, 0, node); |
| 258 node.lightParent = container; |
| 259 } |
| 260 }, |
| 261 |
| 262 // NOTE: in general, we expect contents of the lists here to be small-ish |
| 263 // and therefore indexOf to be nbd. Other optimizations can be made |
| 264 // for larger lists (linked list) |
| 265 _removeLogicalInfo: function(node, container) { |
| 266 var children = factory(container).childNodes; |
| 267 var index = children.indexOf(node); |
| 268 if ((index < 0) || (container !== node.lightParent)) { |
| 269 throw Error('The node to be removed is not a child of this node'); |
| 270 } |
| 271 children.splice(index, 1); |
| 272 node.lightParent = null; |
| 273 }, |
| 274 |
| 275 _removeOwnerShadyRoot: function(node) { |
| 276 // TODO(sorvell): need to clear any children of element? |
| 277 node._ownerShadyRoot = undefined; |
| 278 }, |
| 279 |
| 280 // TODO(sorvell): This will fail if distribution that affects this |
| 281 // question is pending; this is expected to be exceedingly rare, but if |
| 282 // the issue comes up, we can force a flush in this case. |
| 283 _firstComposedNode: function(content) { |
| 284 var n$ = factory(content).getDistributedNodes(); |
| 285 for (var i=0, l=n$.length, n, p$; (i<l) && (n=n$[i]); i++) { |
| 286 p$ = factory(n).getDestinationInsertionPoints(); |
| 287 // means that we're composed to this spot. |
| 288 if (p$[p$.length-1] === content) { |
| 289 return n; |
| 290 } |
| 291 } |
| 292 }, |
| 293 |
| 294 // TODO(sorvell): consider doing native QSA and filtering results. |
| 295 querySelector: function(selector) { |
| 296 return this.querySelectorAll(selector)[0]; |
| 297 }, |
| 298 |
| 299 querySelectorAll: function(selector) { |
| 300 return this._query(function(n) { |
| 301 return matchesSelector.call(n, selector); |
| 302 }, this.node); |
| 303 }, |
| 304 |
| 305 _query: function(matcher, node) { |
| 306 var list = []; |
| 307 this._queryElements(factory(node).childNodes, matcher, list); |
| 308 return list; |
| 309 }, |
| 310 |
| 311 _queryElements: function(elements, matcher, list) { |
| 312 for (var i=0, l=elements.length, c; (i<l) && (c=elements[i]); i++) { |
| 313 if (c.nodeType === Node.ELEMENT_NODE) { |
| 314 this._queryElement(c, matcher, list); |
| 315 } |
| 316 } |
| 317 }, |
| 318 |
| 319 _queryElement: function(node, matcher, list) { |
| 320 if (matcher(node)) { |
| 321 list.push(node); |
| 322 } |
| 323 this._queryElements(factory(node).childNodes, matcher, list); |
| 324 }, |
| 325 |
| 326 getDestinationInsertionPoints: function() { |
| 327 return this.node._destinationInsertionPoints || []; |
| 328 }, |
| 329 |
| 330 getDistributedNodes: function() { |
| 331 return this.node._distributedNodes || []; |
| 332 }, |
| 333 |
| 334 /* |
| 335 Returns a list of nodes distributed within this element. These can be |
| 336 dom children or elements distributed to children that are insertion |
| 337 points. |
| 338 */ |
| 339 queryDistributedElements: function(selector) { |
| 340 var c$ = this.childNodes; |
| 341 var list = []; |
| 342 this._distributedFilter(selector, c$, list); |
| 343 for (var i=0, l=c$.length, c; (i<l) && (c=c$[i]); i++) { |
| 344 if (c.localName === CONTENT) { |
| 345 this._distributedFilter(selector, factory(c).getDistributedNodes(), |
| 346 list); |
| 347 } |
| 348 } |
| 349 return list; |
| 350 }, |
| 351 |
| 352 _distributedFilter: function(selector, list, results) { |
| 353 results = results || []; |
| 354 for (var i=0, l=list.length, d; (i<l) && (d=list[i]); i++) { |
| 355 if ((d.nodeType === Node.ELEMENT_NODE) && |
| 356 (d.localName !== CONTENT) && |
| 357 matchesSelector.call(d, selector)) { |
| 358 results.push(d); |
| 359 } |
| 360 } |
| 361 return results; |
| 362 } |
| 363 |
| 364 }; |
| 365 |
| 366 if (Settings.useShadow) { |
| 367 |
| 368 DomApi.prototype.querySelectorAll = function(selector) { |
| 369 return Array.prototype.slice.call(this.node.querySelectorAll(selector)); |
| 370 }; |
| 371 |
| 372 DomApi.prototype.patch = function() {}; |
| 373 |
| 374 DomApi.prototype.getOwnerRoot = function() { |
| 375 var n = this.node; |
| 376 while (n) { |
| 377 if (n.nodeType === Node.DOCUMENT_FRAGMENT_NODE && n.host) { |
| 378 return n; |
| 379 } |
| 380 n = n.parentNode; |
| 381 } |
| 382 }; |
| 383 |
| 384 DomApi.prototype.getDestinationInsertionPoints = function() { |
| 385 var n$ = this.node.getDestinationInsertionPoints(); |
| 386 return n$ ? Array.prototype.slice.call(n$) : []; |
| 387 }; |
| 388 |
| 389 DomApi.prototype.getDistributedNodes = function() { |
| 390 var n$ = this.node.getDistributedNodes(); |
| 391 return n$ ? Array.prototype.slice.call(n$) : []; |
| 392 }; |
| 393 |
| 394 |
| 395 } |
| 396 |
| 397 var CONTENT = 'content'; |
| 398 |
| 399 var factory = function(node, patch) { |
| 400 node = node || document; |
| 401 if (!node.__domApi) { |
| 402 node.__domApi = new DomApi(node, patch); |
| 403 } |
| 404 return node.__domApi; |
| 405 }; |
| 406 |
| 407 Polymer.dom = function(obj, patch) { |
| 408 if (obj instanceof Event) { |
| 409 return Polymer.EventApi.factory(obj); |
| 410 } else { |
| 411 return factory(obj, patch); |
| 412 } |
| 413 }; |
| 414 |
| 415 // make flush available directly. |
| 416 Polymer.dom.flush = DomApi.prototype.flush; |
| 417 |
| 418 function getLightChildren(node) { |
| 419 var children = node.lightChildren; |
| 420 return children ? children : node.childNodes; |
| 421 } |
| 422 |
| 423 function saveLightChildrenIfNeeded(node) { |
| 424 // Capture the list of light children. It's important to do this before we |
| 425 // start transforming the DOM into "rendered" state. |
| 426 // |
| 427 // Children may be added to this list dynamically. It will be treated as t
he |
| 428 // source of truth for the light children of the element. This element's |
| 429 // actual children will be treated as the rendered state once lightChildre
n |
| 430 // is populated. |
| 431 if (!node.lightChildren) { |
| 432 var children = []; |
| 433 for (var child = node.firstChild; child; child = child.nextSibling) { |
| 434 children.push(child); |
| 435 child.lightParent = child.lightParent || node; |
| 436 } |
| 437 node.lightChildren = children; |
| 438 } |
| 439 } |
| 440 |
| 441 function hasInsertionPoint(root) { |
| 442 return Boolean(root._insertionPoints.length); |
| 443 } |
| 444 |
| 445 var p = Element.prototype; |
| 446 var matchesSelector = p.matches || p.matchesSelector || |
| 447 p.mozMatchesSelector || p.msMatchesSelector || |
| 448 p.oMatchesSelector || p.webkitMatchesSelector; |
| 449 |
| 450 return { |
| 451 getLightChildren: getLightChildren, |
| 452 saveLightChildrenIfNeeded: saveLightChildrenIfNeeded, |
| 453 matchesSelector: matchesSelector, |
| 454 hasInsertionPoint: hasInsertionPoint, |
| 455 factory: factory |
| 456 }; |
| 457 |
| 458 })(); |
| 459 |
| OLD | NEW |