diff --git a/src/ShadowRenderer.js b/src/ShadowRenderer.js index 0cc0db4..96b66ef 100644 --- a/src/ShadowRenderer.js +++ b/src/ShadowRenderer.js @@ -97,29 +97,18 @@ parentNode.removeChild(node); } - var distributedChildNodesTable = new WeakMap(); - var eventParentsTable = new WeakMap(); - var insertionParentTable = new WeakMap(); + var distributedNodesTable = new WeakMap(); + var destinationInsertionPointsTable = new WeakMap(); var rendererForHostTable = new WeakMap(); - function distributeChildToInsertionPoint(child, insertionPoint) { - getDistributedChildNodes(insertionPoint).push(child); - assignToInsertionPoint(child, insertionPoint); - - var eventParents = eventParentsTable.get(child); - if (!eventParents) - eventParentsTable.set(child, eventParents = []); - eventParents.push(insertionPoint); - } - - function resetDistributedChildNodes(insertionPoint) { - distributedChildNodesTable.set(insertionPoint, []); + function resetDistributedNodes(insertionPoint) { + distributedNodesTable.set(insertionPoint, []); } - function getDistributedChildNodes(insertionPoint) { - var rv = distributedChildNodesTable.get(insertionPoint); + function getDistributedNodes(insertionPoint) { + var rv = distributedNodesTable.get(insertionPoint); if (!rv) - distributedChildNodesTable.set(insertionPoint, rv = []); + distributedNodesTable.set(insertionPoint, rv = []); return rv; } @@ -131,92 +120,6 @@ return result; } - /** - * Visits all nodes in the tree that fulfils the |predicate|. If the |visitor| - * function returns |false| the traversal is aborted. - * @param {!Node} tree - * @param {function(!Node) : boolean} predicate - * @param {function(!Node) : *} visitor - */ - function visit(tree, predicate, visitor) { - // This operates on logical DOM. - for (var node = tree.firstChild; node; node = node.nextSibling) { - if (predicate(node)) { - if (visitor(node) === false) - return; - } else { - visit(node, predicate, visitor); - } - } - } - - // Matching Insertion Points - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#matching-insertion-points - - // TODO(arv): Verify this... I don't remember why I picked this regexp. - var selectorMatchRegExp = /^[*.:#[a-zA-Z_|]/; - - var allowedPseudoRegExp = new RegExp('^:(' + [ - 'link', - 'visited', - 'target', - 'enabled', - 'disabled', - 'checked', - 'indeterminate', - 'nth-child', - 'nth-last-child', - 'nth-of-type', - 'nth-last-of-type', - 'first-child', - 'last-child', - 'first-of-type', - 'last-of-type', - 'only-of-type', - ].join('|') + ')'); - - - /** - * @param {Element} node - * @oaram {Element} point The insertion point element. - * @return {boolean} Whether the node matches the insertion point. - */ - function matchesCriteria(node, point) { - var select = point.getAttribute('select'); - if (!select) - return true; - - // Here we know the select attribute is a non empty string. - select = select.trim(); - if (!select) - return true; - - if (!(node instanceof Element)) - return false; - - // The native matches function in IE9 does not correctly work with elements - // that are not in the document. - // TODO(arv): Implement matching in JS. - // https://github.com/Polymer/ShadowDOM/issues/361 - if (select === '*' || select === node.localName) - return true; - - // TODO(arv): This does not seem right. Need to check for a simple selector. - if (!selectorMatchRegExp.test(select)) - return false; - - // TODO(arv): This no longer matches the spec. - if (select[0] === ':' && !allowedPseudoRegExp.test(select)) - return false; - - try { - return node.matches(select); - } catch (ex) { - // Invalid selector. - return false; - } - } - var request = oneOf(window, [ 'requestAnimationFrame', 'mozRequestAnimationFrame', @@ -361,19 +264,14 @@ return; this.invalidateAttributes(); - this.treeComposition(); var host = this.host; - var shadowRoot = host.shadowRoot; - this.associateNode(host); - var topMostRenderer = !renderNode; + this.distribution(host); var renderNode = opt_renderNode || new RenderNode(host); + this.buildRenderTree(renderNode, host); - for (var node = shadowRoot.firstChild; node; node = node.nextSibling) { - this.renderNode(shadowRoot, renderNode, node, false); - } - + var topMostRenderer = !opt_renderNode; if (topMostRenderer) renderNode.sync(); @@ -394,77 +292,152 @@ } }, - renderNode: function(shadowRoot, renderNode, node, isNested) { - if (isShadowHost(node)) { - renderNode = renderNode.append(node); - var renderer = getRendererForHost(node); - renderer.dirty = true; // Need to rerender due to reprojection. - renderer.render(renderNode); - } else if (isInsertionPoint(node)) { - this.renderInsertionPoint(shadowRoot, renderNode, node, isNested); - } else if (isShadowInsertionPoint(node)) { - this.renderShadowInsertionPoint(shadowRoot, renderNode, node); - } else { - this.renderAsAnyDomTree(shadowRoot, renderNode, node, isNested); - } + // http://w3c.github.io/webcomponents/spec/shadow/#distribution-algorithms + distribution: function(root) { + this.resetAll(root); + this.distributionResolution(root); }, - renderAsAnyDomTree: function(shadowRoot, renderNode, node, isNested) { - renderNode = renderNode.append(node); + resetAll: function(node) { + if (isInsertionPoint(node)) + resetDistributedNodes(node); + else + resetDestinationInsertionPoints(node); + for (var child = node.firstChild; child; child = child.nextSibling) { + this.resetAll(child); + } + + if (node.shadowRoot) + this.resetAll(node.shadowRoot); + + if (node.olderShadowRoot) + this.resetAll(node.olderShadowRoot); + }, + + // http://w3c.github.io/webcomponents/spec/shadow/#distribution-results + distributionResolution: function(node) { if (isShadowHost(node)) { - var renderer = getRendererForHost(node); - renderNode.skip = !renderer.dirty; - renderer.render(renderNode); - } else { - for (var child = node.firstChild; child; child = child.nextSibling) { - this.renderNode(shadowRoot, renderNode, child, isNested); + var shadowHost = node; + // 1.1 + var pool = poolPopulation(shadowHost); + + var shadowTrees = getShadowTrees(shadowHost); + + // 1.2 + for (var i = 0; i < shadowTrees.length; i++) { + // 1.2.1 + this.poolDistribution(shadowTrees[i], pool); } + + // 1.3 + for (var i = shadowTrees.length - 1; i >= 0; i--) { + var shadowTree = shadowTrees[i]; + + // 1.3.1 + // TODO(arv): We should keep the shadow insertion points on the + // shadow root (or renderer) so we don't have to search the tree + // every time. + var shadow = getShadowInsertionPoint(shadowTree); + + // 1.3.2 + if (shadow) { + + // 1.3.2.1 + var olderShadowRoot = shadowTree.olderShadowRoot; + if (olderShadowRoot) { + // 1.3.2.1.1 + pool = poolPopulation(olderShadowRoot); + } + + // 1.3.2.2 + for (var j = 0; j < pool.length; j++) { + // 1.3.2.2.1 + destributeNodeInto(pool[j], shadow); + } + + } else { + // 1.3.3 + this.distributionResolution(shadowTree); + } + } + } + for (var child = node.firstChild; child; child = child.nextSibling) { + this.distributionResolution(child); } }, - renderInsertionPoint: function(shadowRoot, renderNode, insertionPoint, - isNested) { - var distributedChildNodes = getDistributedChildNodes(insertionPoint); - if (distributedChildNodes.length) { - this.associateNode(insertionPoint); - - for (var i = 0; i < distributedChildNodes.length; i++) { - var child = distributedChildNodes[i]; - if (isInsertionPoint(child) && isNested) - this.renderInsertionPoint(shadowRoot, renderNode, child, isNested); - else - this.renderAsAnyDomTree(shadowRoot, renderNode, child, isNested); + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-distribution-algorithm + poolDistribution: function (node, pool) { + if (node instanceof HTMLShadowElement) + return; + + if (node instanceof HTMLContentElement) { + var content = node; + this.updateDependentAttributes(content.getAttribute('select')); + + // 1.1 + for (var i = 0; i < pool.length; i++) { + var node = pool[i]; + if (!node) + continue; + if (matches(node, content)) { + destributeNodeInto(node, content); + pool[i] = undefined; + } } - } else { - this.renderFallbackContent(shadowRoot, renderNode, insertionPoint); + + // 1.2 + // Fallback content + var distributedNodes = getDistributedNodes(content); + if (distributedNodes && distributedNodes.length === 0) { + for (var child = content.firstChild; + child; + child = child.nextSibling) { + destributeNodeInto(child, content); + } + } + + return; + } + + for (var child = node.firstChild; child; child = child.nextSibling) { + this.poolDistribution(child, pool); } - this.associateNode(insertionPoint.parentNode); }, - renderShadowInsertionPoint: function(shadowRoot, renderNode, - shadowInsertionPoint) { - var nextOlderTree = shadowRoot.olderShadowRoot; - if (nextOlderTree) { - assignToInsertionPoint(nextOlderTree, shadowInsertionPoint); - this.associateNode(shadowInsertionPoint.parentNode); - for (var node = nextOlderTree.firstChild; - node; - node = node.nextSibling) { - this.renderNode(nextOlderTree, renderNode, node, true); - } - } else { - this.renderFallbackContent(shadowRoot, renderNode, - shadowInsertionPoint); + buildRenderTree: function(renderNode, node) { + var children = this.compose(node); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var childRenderNode = renderNode.append(child); + this.buildRenderTree(childRenderNode, child); + } + + if (isShadowHost(node)) { + var renderer = getRendererForHost(node); + renderer.dirty = false; } + }, - renderFallbackContent: function(shadowRoot, renderNode, fallbackHost) { - this.associateNode(fallbackHost); - this.associateNode(fallbackHost.parentNode); - for (var node = fallbackHost.firstChild; node; node = node.nextSibling) { - this.renderAsAnyDomTree(shadowRoot, renderNode, node, false); + compose: function(node) { + var children = []; + var p = node.shadowRoot || node; + for (var child = p.firstChild; child; child = child.nextSibling) { + if (isInsertionPoint(child)) { + this.associateNode(p); + var distributedNodes = getDistributedNodes(child); + for (var j = 0; j < distributedNodes.length; j++) { + var distributedNode = distributedNodes[j]; + if (isFinalDestination(child, distributedNode)) + children.push(distributedNode); + } + } else { + children.push(child); + } } + return children; }, /** @@ -505,102 +478,103 @@ return this.attributes[name]; }, - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-distribution-algorithm - distribute: function(tree, pool) { - var self = this; - - visit(tree, isActiveInsertionPoint, - function(insertionPoint) { - resetDistributedChildNodes(insertionPoint); - self.updateDependentAttributes( - insertionPoint.getAttribute('select')); - - for (var i = 0; i < pool.length; i++) { // 1.2 - var node = pool[i]; // 1.2.1 - if (node === undefined) // removed - continue; - if (matchesCriteria(node, insertionPoint)) { // 1.2.2 - distributeChildToInsertionPoint(node, insertionPoint); // 1.2.2.1 - pool[i] = undefined; // 1.2.2.2 - } - } - }); - }, - - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-tree-composition - treeComposition: function () { - var shadowHost = this.host; - var tree = shadowHost.shadowRoot; // 1. - var pool = []; // 2. - - for (var child = shadowHost.firstChild; - child; - child = child.nextSibling) { // 3. - if (isInsertionPoint(child)) { // 3.2. - var reprojected = getDistributedChildNodes(child); // 3.2.1. - // if reprojected is undef... reset it? - if (!reprojected || !reprojected.length) // 3.2.2. - reprojected = getChildNodesSnapshot(child); - pool.push.apply(pool, reprojected); // 3.2.3. - } else { - pool.push(child); // 3.3. - } - } - - var shadowInsertionPoint, point; - while (tree) { // 4. - // 4.1. - shadowInsertionPoint = undefined; // Reset every iteration. - visit(tree, isActiveShadowInsertionPoint, function(point) { - shadowInsertionPoint = point; - return false; - }); - point = shadowInsertionPoint; - - this.distribute(tree, pool); // 4.2. - if (point) { // 4.3. - var nextOlderTree = tree.olderShadowRoot; // 4.3.1. - if (!nextOlderTree) { - break; // 4.3.1.1. - } else { - tree = nextOlderTree; // 4.3.2.2. - assignToInsertionPoint(tree, point); // 4.3.2.2. - continue; // 4.3.2.3. - } - } else { - break; // 4.4. - } - } - }, - associateNode: function(node) { node.impl.polymerShadowRenderer_ = this; } }; - function isInsertionPoint(node) { - // Should this include ? - return node instanceof HTMLContentElement; + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-population-algorithm + function poolPopulation(node) { + var pool = []; + for (var child = node.firstChild; child; child = child.nextSibling) { + if (isInsertionPoint(child)) { + pool.push.apply(pool, getDistributedNodes(child)); + } else { + pool.push(child); + } + } + return pool; + } + + function getShadowInsertionPoint(node) { + if (node instanceof HTMLShadowElement) + return node; + if (node instanceof HTMLContentElement) + return null; + for (var child = node.firstChild; child; child = child.nextSibling) { + var res = getShadowInsertionPoint(child); + if (res) + return res; + } + return null; + } + + function destributeNodeInto(child, insertionPoint) { + getDistributedNodes(insertionPoint).push(child); + var points = destinationInsertionPointsTable.get(child); + if (!points) + destinationInsertionPointsTable.set(child, [insertionPoint]); + else + points.push(insertionPoint); + } + + function getDestinationInsertionPoints(node) { + return destinationInsertionPointsTable.get(node); + } + + function resetDestinationInsertionPoints(node) { + // IE11 crashes when delete is used. + destinationInsertionPointsTable.set(node, undefined); } - function isActiveInsertionPoint(node) { - // inside another or is considered inactive. - return node instanceof HTMLContentElement; + // AllowedSelectors : + // TypeSelector + // * + // ClassSelector + // IDSelector + // AttributeSelector + var selectorStartCharRe = /^[*.#[a-zA-Z_|]/; + + function matches(node, contentElement) { + var select = contentElement.getAttribute('select'); + if (!select) + return true; + + // Here we know the select attribute is a non empty string. + select = select.trim(); + if (!select) + return true; + + if (!(node instanceof Element)) + return false; + + if (!selectorStartCharRe.test(select)) + return false; + + try { + return node.matches(select); + } catch (ex) { + // Invalid selector. + return false; + } } - function isShadowInsertionPoint(node) { - return node instanceof HTMLShadowElement; + function isFinalDestination(insertionPoint, node) { + var points = getDestinationInsertionPoints(node); + return points && points[points.length - 1] === insertionPoint; } - function isActiveShadowInsertionPoint(node) { - // inside another or is considered inactive. - return node instanceof HTMLShadowElement; + function isInsertionPoint(node) { + return node instanceof HTMLContentElement || + node instanceof HTMLShadowElement; } function isShadowHost(shadowHost) { return shadowHost.shadowRoot; } + // Returns the shadow trees as an array, with the youngest tree at the + // beginning of the array. function getShadowTrees(host) { var trees = []; @@ -610,11 +584,6 @@ return trees; } - function assignToInsertionPoint(tree, point) { - insertionParentTable.set(tree, point); - } - - // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees function render(host) { new ShadowRenderer(host).render(); }; @@ -642,15 +611,21 @@ return false; }; - HTMLContentElement.prototype.getDistributedNodes = function() { + HTMLContentElement.prototype.getDistributedNodes = + HTMLShadowElement.prototype.getDistributedNodes = function() { // TODO(arv): We should only rerender the dirty ancestor renderers (from // the root and down). renderAllPending(); - return getDistributedChildNodes(this); + return getDistributedNodes(this); }; - HTMLShadowElement.prototype.nodeIsInserted_ = - HTMLContentElement.prototype.nodeIsInserted_ = function() { + Element.prototype.getDestinationInsertionPoints = function() { + renderAllPending(); + return getDestinationInsertionPoints(this) || []; + }; + + HTMLContentElement.prototype.nodeIsInserted_ = + HTMLShadowElement.prototype.nodeIsInserted_ = function() { // Invalidate old renderer if any. this.invalidateShadowRenderer(); @@ -663,12 +638,12 @@ renderer.invalidate(); }; - scope.eventParentsTable = eventParentsTable; scope.getRendererForHost = getRendererForHost; scope.getShadowTrees = getShadowTrees; - scope.insertionParentTable = insertionParentTable; scope.renderAllPending = renderAllPending; + scope.getDestinationInsertionPoints = getDestinationInsertionPoints; + // Exposed for testing scope.visual = { insertBefore: insertBefore, diff --git a/src/TreeScope.js b/src/TreeScope.js index 4e59188..5bdada9 100644 --- a/src/TreeScope.js +++ b/src/TreeScope.js @@ -11,10 +11,21 @@ * A tree scope represents the root of a tree. All nodes in a tree point to * the same TreeScope object. The tree scope of a node get set the first time * it is accessed or when a node is added or remove to a tree. + * + * The root is a Node that has no parent. + * + * The parent is another TreeScope. For ShadowRoots, it is the TreeScope of + * the host of the ShadowRoot. + * + * @param {!Node} root + * @param {TreeScope} parent * @constructor */ function TreeScope(root, parent) { + /** @type {!Node} */ this.root = root; + + /** @type {TreeScope} */ this.parent = parent; } @@ -48,6 +59,10 @@ } function getTreeScope(node) { + if (node instanceof scope.wrappers.Window) { + debugger; + } + if (node.treeScope_) return node.treeScope_; var parent = node.parentNode; diff --git a/src/wrappers/Document.js b/src/wrappers/Document.js index b6ac111..7876dac 100644 --- a/src/wrappers/Document.js +++ b/src/wrappers/Document.js @@ -264,6 +264,10 @@ new DOMImplementation(unwrap(this).implementation); implementationTable.set(this, implementation); return implementation; + }, + + get defaultView() { + return wrap(unwrap(this).defaultView); } }); diff --git a/src/wrappers/Element.js b/src/wrappers/Element.js index 444775e..c88259c 100644 --- a/src/wrappers/Element.js +++ b/src/wrappers/Element.js @@ -73,6 +73,8 @@ return this.impl.polymerShadowRoot_ || null; }, + // getDestinationInsertionPoints added in ShadowRenderer.js + setAttribute: function(name, value) { var oldValue = this.impl.getAttribute(name); this.impl.setAttribute(name, value); diff --git a/src/wrappers/HTMLContentElement.js b/src/wrappers/HTMLContentElement.js index c4ef3c5..df6cff3 100644 --- a/src/wrappers/HTMLContentElement.js +++ b/src/wrappers/HTMLContentElement.js @@ -30,8 +30,6 @@ } // getDistributedNodes is added in ShadowRenderer - - // TODO: attribute boolean resetStyleInheritance; }); if (OriginalHTMLContentElement) diff --git a/src/wrappers/HTMLShadowElement.js b/src/wrappers/HTMLShadowElement.js index 2116ed5..da7ca12 100644 --- a/src/wrappers/HTMLShadowElement.js +++ b/src/wrappers/HTMLShadowElement.js @@ -7,6 +7,7 @@ var HTMLElement = scope.wrappers.HTMLElement; var mixin = scope.mixin; + var NodeList = scope.wrappers.NodeList; var registerWrapper = scope.registerWrapper; var OriginalHTMLShadowElement = window.HTMLShadowElement; @@ -15,9 +16,8 @@ HTMLElement.call(this, node); } HTMLShadowElement.prototype = Object.create(HTMLElement.prototype); - mixin(HTMLShadowElement.prototype, { - // TODO: attribute boolean resetStyleInheritance; - }); + + // getDistributedNodes is added in ShadowRenderer if (OriginalHTMLShadowElement) registerWrapper(OriginalHTMLShadowElement, HTMLShadowElement); diff --git a/src/wrappers/Window.js b/src/wrappers/Window.js index 5842540..4551cd0 100644 --- a/src/wrappers/Window.js +++ b/src/wrappers/Window.js @@ -56,9 +56,13 @@ renderAllPending(); return new Selection(originalGetSelection.call(unwrap(this))); }, + + get document() { + return wrap(unwrap(this).document); + } }); - registerWrapper(OriginalWindow, Window); + registerWrapper(OriginalWindow, Window, window); scope.wrappers.Window = Window; diff --git a/src/wrappers/events.js b/src/wrappers/events.js index abd551f..e90ecc5 100644 --- a/src/wrappers/events.js +++ b/src/wrappers/events.js @@ -30,141 +30,193 @@ return node instanceof wrappers.ShadowRoot; } - function isInsertionPoint(node) { - var localName = node.localName; - return localName === 'content' || localName === 'shadow'; - } - - function isShadowHost(node) { - return !!node.shadowRoot; - } - - function getEventParent(node) { - var dv; - return node.parentNode || (dv = node.defaultView) && wrap(dv) || null; - } + function rootOfNode(node) { + return getTreeScope(node).root; + } + + // http://w3c.github.io/webcomponents/spec/shadow/#event-paths + function getEventPath(node, event) { + var path = []; + var current = node; + path.push(current); + while (current) { + // 4.1. + var destinationInsertionPoints = getDestinationInsertionPoints(current); + if (destinationInsertionPoints && destinationInsertionPoints.length > 0) { + // 4.1.1 + for (var i = 0; i < destinationInsertionPoints.length; i++) { + var insertionPoint = destinationInsertionPoints[i]; + // 4.1.1.1 + if (isShadowInsertionPoint(insertionPoint)) { + var shadowRoot = rootOfNode(insertionPoint); + // 4.1.1.1.2 + var olderShadowRoot = shadowRoot.olderShadowRoot; + if (olderShadowRoot) + path.push(olderShadowRoot); + } - // https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-adjusted-parent - function calculateParents(node, context, ancestors) { - if (ancestors.length) - return ancestors.shift(); + // 4.1.1.2 + path.push(insertionPoint); + } - // 1. - if (isShadowRoot(node)) - return getInsertionParent(node) || node.host; + // 4.1.2 + current = destinationInsertionPoints[ + destinationInsertionPoints.length - 1]; - // 2. - var eventParents = scope.eventParentsTable.get(node); - if (eventParents) { - // Copy over the remaining event parents for next iteration. - for (var i = 1; i < eventParents.length; i++) { - ancestors[i - 1] = eventParents[i]; - } - return eventParents[0]; - } + // 4.2 + } else { + if (isShadowRoot(current)) { + if (inSameTree(node, current) && eventMustBeStopped(event)) { + // Stop this algorithm + break; + } + current = current.host; + path.push(current); - // 3. - if (context && isInsertionPoint(node)) { - var parentNode = node.parentNode; - if (parentNode && isShadowHost(parentNode)) { - var trees = scope.getShadowTrees(parentNode); - var p = getInsertionParent(context); - for (var i = 0; i < trees.length; i++) { - if (trees[i].contains(p)) - return p; + // 4.2.2 + } else { + current = current.parentNode; + if (current) + path.push(current); } } } - return getEventParent(node); + return path; } - // https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#event-retargeting - function retarget(node) { - var stack = []; // 1. - var ancestor = node; // 2. - var targets = []; - var ancestors = []; - while (ancestor) { // 3. - var context = null; // 3.2. - // TODO(arv): Change order of these. If the stack is empty we always end - // up pushing ancestor, no matter what. - if (isInsertionPoint(ancestor)) { // 3.1. - context = topMostNotInsertionPoint(stack); // 3.1.1. - var top = stack[stack.length - 1] || ancestor; // 3.1.2. - stack.push(top); - } else if (!stack.length) { - stack.push(ancestor); // 3.3. - } - var target = stack[stack.length - 1]; // 3.4. - targets.push({target: target, currentTarget: ancestor}); // 3.5. - if (isShadowRoot(ancestor)) // 3.6. - stack.pop(); // 3.6.1. + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-events-always-stopped + function eventMustBeStopped(event) { + if (!event) + return false; - ancestor = calculateParents(ancestor, context, ancestors); // 3.7. + switch (event.type) { + case 'abort': + case 'error': + case 'select': + case 'change': + case 'load': + case 'reset': + case 'resize': + case 'scroll': + case 'selectstart': + return true; } - return targets; + return false; } - function topMostNotInsertionPoint(stack) { - for (var i = stack.length - 1; i >= 0; i--) { - if (!isInsertionPoint(stack[i])) - return stack[i]; + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-shadow-insertion-point + function isShadowInsertionPoint(node) { + return node instanceof HTMLShadowElement; + // and make sure that there are no shadow precing this? + // and that there is no content ancestor? + } + + function getDestinationInsertionPoints(node) { + return scope.getDestinationInsertionPoints(node); + } + + // http://w3c.github.io/webcomponents/spec/shadow/#event-retargeting + function eventRetargetting(path, currentTarget) { + if (path.length === 0) + return currentTarget; + + // The currentTarget might be the window object. Use its document for the + // purpose of finding the retargetted node. + if (currentTarget instanceof wrappers.Window) + currentTarget = currentTarget.document; + + var currentTargetTree = getTreeScope(currentTarget); + var originalTarget = path[0]; + var originalTargetTree = getTreeScope(originalTarget); + var relativeTargetTree = + lowestCommonInclusiveAncestor(currentTargetTree, originalTargetTree); + + for (var i = 0; i < path.length; i++) { + var node = path[i]; + if (getTreeScope(node) === relativeTargetTree) + return node; } - return null; + + return path[path.length - 1]; } - // https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#dfn-adjusted-related-target - function adjustRelatedTarget(target, related) { + function getTreeScopeAncestors(treeScope) { var ancestors = []; - while (target) { // 3. - var stack = []; // 3.1. - var ancestor = related; // 3.2. - var last = undefined; // 3.3. Needs to be reset every iteration. - while (ancestor) { - var context = null; - if (!stack.length) { - stack.push(ancestor); - } else { - if (isInsertionPoint(ancestor)) { // 3.4.3. - context = topMostNotInsertionPoint(stack); - // isDistributed is more general than checking whether last is - // assigned into ancestor. - if (isDistributed(last)) { // 3.4.3.2. - var head = stack[stack.length - 1]; - stack.push(head); - } - } - } - - if (inSameTree(ancestor, target)) // 3.4.4. - return stack[stack.length - 1]; + for (;treeScope; treeScope = treeScope.parent) { + ancestors.push(treeScope); + } + return ancestors; + } - if (isShadowRoot(ancestor)) // 3.4.5. - stack.pop(); + function lowestCommonInclusiveAncestor(tsA, tsB) { + var ancestorsA = getTreeScopeAncestors(tsA); + var ancestorsB = getTreeScopeAncestors(tsB); - last = ancestor; // 3.4.6. - ancestor = calculateParents(ancestor, context, ancestors); // 3.4.7. - } - if (isShadowRoot(target)) // 3.5. - target = target.host; + var result = null; + while (ancestorsA.length > 0 && ancestorsB.length > 0) { + var a = ancestorsA.pop(); + var b = ancestorsB.pop(); + if (a === b) + result = a; else - target = target.parentNode; // 3.6. + break; } + return result; } - function getInsertionParent(node) { - return scope.insertionParentTable.get(node); + function getTreeScopeRoot(ts) { + if (!ts.parent) + return ts; + return getTreeScopeRoot(ts.parent); } - function isDistributed(node) { - return getInsertionParent(node); + function relatedTargetResolution(event, currentTarget, relatedTarget) { + // In case the current target is a window use its document for the purpose + // of retargetting the related target. + if (currentTarget instanceof wrappers.Window) + currentTarget = currentTarget.document; + + var currentTargetTree = getTreeScope(currentTarget); + var relatedTargetTree = getTreeScope(relatedTarget); + + var relatedTargetEventPath = getEventPath(relatedTarget, event); + + var lowestCommonAncestorTree; + + // 4 + var lowestCommonAncestorTree = + lowestCommonInclusiveAncestor(currentTargetTree, relatedTargetTree); + + // 5 + if (!lowestCommonAncestorTree) + lowestCommonAncestorTree = relatedTargetTree.root; + + // 6 + for (var commonAncestorTree = lowestCommonAncestorTree; + commonAncestorTree; + commonAncestorTree = commonAncestorTree.parent) { + // 6.1 + var adjustedRelatedTarget; + for (var i = 0; i < relatedTargetEventPath.length; i++) { + var node = relatedTargetEventPath[i]; + if (getTreeScope(node) === commonAncestorTree) + return node; + } + } + + return null; } function inSameTree(a, b) { return getTreeScope(a) === getTreeScope(b); } + var NONE = 0; + var CAPTURING_PHASE = 1; + var AT_TARGET = 2; + var BUBBLING_PHASE = 3; + // pendingError is used to rethrow the first error we got during an event // dispatch. The browser actually reports all errors but to do that we would // need to rethrow the error asynchronously. @@ -183,101 +235,118 @@ } } - function isLoadLikeEvent(event) { - switch (event.type) { - case 'beforeunload': - case 'load': - case 'unload': - return true; - } - return false; - } - function dispatchEvent(event, originalWrapperTarget) { if (currentlyDispatchingEvents.get(event)) - throw new Error('InvalidStateError') + throw new Error('InvalidStateError'); + currentlyDispatchingEvents.set(event, true); // Render to ensure that the event path is correct. scope.renderAllPending(); - var eventPath = retarget(originalWrapperTarget); + var eventPath; + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#events-and-the-window-object + // All events dispatched on Nodes with a default view, except load events, + // should propagate to the Window. - // For window "load" events the "load" event is dispatched at the window but - // the target is set to the document. - // // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#the-end - // - // TODO(arv): Find a less hacky way to do this. - if (eventPath.length === 2 && - eventPath[0].target instanceof wrappers.Document && - isLoadLikeEvent(event)) { - eventPath.shift(); + var overrideTarget; + var win; + var type = event.type; + + // Should really be not cancelable too but since Firefox has a bug there + // we skip that check. + // https://bugzilla.mozilla.org/show_bug.cgi?id=999456 + if (type === 'load' && !event.bubbles) { + var doc = originalWrapperTarget; + if (doc instanceof wrappers.Document && (win = doc.defaultView)) { + overrideTarget = doc; + eventPath = []; + } + } + + if (!eventPath) { + if (originalWrapperTarget instanceof wrappers.Window) { + win = originalWrapperTarget; + eventPath = []; + } else { + eventPath = getEventPath(originalWrapperTarget, event); + + if (event.type !== 'load') { + var doc = eventPath[eventPath.length - 1]; + if (doc instanceof wrappers.Document) + win = doc.defaultView; + } + } } eventPathTable.set(event, eventPath); - if (dispatchCapturing(event, eventPath)) { - if (dispatchAtTarget(event, eventPath)) { - dispatchBubbling(event, eventPath); + if (dispatchCapturing(event, eventPath, win, overrideTarget)) { + if (dispatchAtTarget(event, eventPath, win, overrideTarget)) { + dispatchBubbling(event, eventPath, win, overrideTarget); } } - eventPhaseTable.set(event, Event.NONE); + eventPhaseTable.set(event, NONE); currentTargetTable.delete(event, null); currentlyDispatchingEvents.delete(event); return event.defaultPrevented; } - function dispatchCapturing(event, eventPath) { - var phase; + function dispatchCapturing(event, eventPath, win, overrideTarget) { + var phase = CAPTURING_PHASE; - for (var i = eventPath.length - 1; i > 0; i--) { - var target = eventPath[i].target; - var currentTarget = eventPath[i].currentTarget; - if (target === currentTarget) - continue; + if (win) { + if (!invoke(win, event, phase, eventPath, overrideTarget)) + return false; + } - phase = Event.CAPTURING_PHASE; - if (!invoke(eventPath[i], event, phase)) + for (var i = eventPath.length - 1; i > 0; i--) { + if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) return false; } return true; } - function dispatchAtTarget(event, eventPath) { - var phase = Event.AT_TARGET; - return invoke(eventPath[0], event, phase); + function dispatchAtTarget(event, eventPath, win, overrideTarget) { + var phase = AT_TARGET; + var currentTarget = eventPath[0] || win; + return invoke(currentTarget, event, phase, eventPath, overrideTarget); } - function dispatchBubbling(event, eventPath) { - var bubbles = event.bubbles; - var phase; - + function dispatchBubbling(event, eventPath, win, overrideTarget) { + var phase = BUBBLING_PHASE; for (var i = 1; i < eventPath.length; i++) { - var target = eventPath[i].target; - var currentTarget = eventPath[i].currentTarget; - if (target === currentTarget) - phase = Event.AT_TARGET; - else if (bubbles && !stopImmediatePropagationTable.get(event)) - phase = Event.BUBBLING_PHASE; - else - continue; - - if (!invoke(eventPath[i], event, phase)) + if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) return; } - } - function invoke(tuple, event, phase) { - var target = tuple.target; - var currentTarget = tuple.currentTarget; + if (win && eventPath.length > 0) { + invoke(win, event, phase, eventPath, overrideTarget); + } + } + function invoke(currentTarget, event, phase, eventPath, overrideTarget) { var listeners = listenersTable.get(currentTarget); if (!listeners) return true; + var target = overrideTarget || eventRetargetting(eventPath, currentTarget); + + if (target === currentTarget) { + if (phase === CAPTURING_PHASE) + return true; + + if (phase === BUBBLING_PHASE) + phase = AT_TARGET; + + } else if (phase === BUBBLING_PHASE && !event.bubbles) { + return true; + } + if ('relatedTarget' in event) { var originalEvent = unwrap(event); var unwrappedRelatedTarget = originalEvent.relatedTarget; @@ -294,7 +363,8 @@ unwrappedRelatedTarget.addEventListener) { var relatedTarget = wrap(unwrappedRelatedTarget); - var adjusted = adjustRelatedTarget(currentTarget, relatedTarget); + var adjusted = + relatedTargetResolution(event, currentTarget, relatedTarget); if (adjusted === target) return true; } else { @@ -308,6 +378,7 @@ var type = event.type; var anyRemoved = false; + // targetTable.set(event, target); targetTable.set(event, target); currentTargetTable.set(event, currentTarget); @@ -319,8 +390,8 @@ } if (listener.type !== type || - !listener.capture && phase === Event.CAPTURING_PHASE || - listener.capture && phase === Event.BUBBLING_PHASE) { + !listener.capture && phase === CAPTURING_PHASE || + listener.capture && phase === BUBBLING_PHASE) { continue; } @@ -412,7 +483,7 @@ var baseRoot = getTreeScope(currentTargetTable.get(this)); for (var i = 0; i <= lastIndex; i++) { - var currentTarget = eventPath[i].currentTarget; + var currentTarget = eventPath[i]; var currentRoot = getTreeScope(currentTarget); if (currentRoot.contains(baseRoot) && // Make sure we do not add Window to the path. @@ -755,13 +826,8 @@ scope.renderAllPending(); var element = wrap(originalElementFromPoint.call(document.impl, x, y)); - var targets = retarget(element, this) - for (var i = 0; i < targets.length; i++) { - var target = targets[i]; - if (target.currentTarget === self) - return target.target; - } - return null; + var path = getEventPath(element, null); + return eventRetargetting(path, self); } /** @@ -815,7 +881,6 @@ }; } - scope.adjustRelatedTarget = adjustRelatedTarget; scope.elementFromPoint = elementFromPoint; scope.getEventHandlerGetter = getEventHandlerGetter; scope.getEventHandlerSetter = getEventHandlerSetter; diff --git a/test/js/Element.js b/test/js/Element.js index f5a9c27..b745b7d 100644 --- a/test/js/Element.js +++ b/test/js/Element.js @@ -165,4 +165,41 @@ suite('Element', function() { var sr = div.webkitCreateShadowRoot(); assert.instanceOf(sr, ShadowRoot); }); + + test('getDestinationInsertionPoints', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var content = sr.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([content], b.getDestinationInsertionPoints()); + + var sr2 = div.createShadowRoot(); + sr2.innerHTML = ''; + var contentB = sr2.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([contentB], b.getDestinationInsertionPoints()); + }); + + test('getDestinationInsertionPoints redistribution', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + var content = c.firstChild; + var sr2 = c.createShadowRoot(); + sr2.innerHTML = ''; + var contentB = sr2.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([content, contentB], b.getDestinationInsertionPoints()); + }); }); diff --git a/test/js/HTMLShadowElement.js b/test/js/HTMLShadowElement.js index e9d8335..583c2a7 100644 --- a/test/js/HTMLShadowElement.js +++ b/test/js/HTMLShadowElement.js @@ -30,7 +30,7 @@ suite('HTMLShadowElement', function() { assert.isTrue(shadow2 instanceof HTMLShadowElement); - assert.equal(unwrap(host).innerHTML, 'dabcf'); + assert.equal(unwrap(host).innerHTML, 'daabcf'); }); test('adding a new shadow element to a shadow tree', function() { @@ -59,7 +59,7 @@ suite('HTMLShadowElement', function() { assert.equal(unwrap(host).innerHTML, ''); }); - test('Mutating shadow fallback', function() { + test('Mutating shadow fallback (fallback support has been removed)', function() { var host = document.createElement('div'); host.innerHTML = ''; var a = host.firstChild; @@ -69,22 +69,23 @@ suite('HTMLShadowElement', function() { var shadow = sr.firstChild; host.offsetHeight; - assert.equal(unwrap(host).innerHTML, ''); + assert.equal(unwrap(host).innerHTML, ''); shadow.textContent = 'fallback'; host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); var b = shadow.appendChild(document.createElement('b')); host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); shadow.removeChild(b); host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); }); - test('Mutating shadow fallback 2', function() { + test('Mutating shadow fallback 2 (fallback support has been removed)', + function() { var host = document.createElement('div'); host.innerHTML = ''; var a = host.firstChild; @@ -95,18 +96,18 @@ suite('HTMLShadowElement', function() { var shadow = b.firstChild; host.offsetHeight; - assert.equal(unwrap(host).innerHTML, ''); + assert.equal(unwrap(host).innerHTML, ''); shadow.textContent = 'fallback'; host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); var c = shadow.appendChild(document.createElement('c')); host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); shadow.removeChild(c); host.offsetHeight; - assert.equal(unwrap(host).innerHTML, 'fallback'); + assert.equal(unwrap(host).innerHTML, ''); }); }); diff --git a/test/js/events.js b/test/js/events.js index 7e2ca25..4ddccac 100644 --- a/test/js/events.js +++ b/test/js/events.js @@ -6,7 +6,6 @@ htmlSuite('Events', function() { - var adjustRelatedTarget = ShadowDOMPolyfill.adjustRelatedTarget; var unwrap = ShadowDOMPolyfill.unwrap; var wrap = ShadowDOMPolyfill.wrap; @@ -401,27 +400,6 @@ htmlSuite('Events', function() { assert.equal(calls, 0); }); - test('adjustRelatedTarget', function() { - var div = document.createElement('div'); - div.innerHTML = ''; - var a = div.firstChild; - var b = div.lastChild; - var c = b.firstChild; - var d = b.lastChild; - - assert.equal(adjustRelatedTarget(c, d), d); - - var sr = b.createShadowRoot(); - sr.innerHTML = ''; - var e = sr.firstChild; - var content = e.nextSibling; - var f = sr.lastChild; - - assert.equal(adjustRelatedTarget(a, e), b); - assert.equal(adjustRelatedTarget(e, f), f); - assert.equal(adjustRelatedTarget(b, f), b); - }); - test('mouseover retarget to host', function() { createTestTree(); @@ -862,14 +840,14 @@ test('retarget order (multiple shadow roots)', function() { var event = new MouseEvent('mouseover', {relatedTarget: c, bubbles: true}); b.dispatchEvent(event); var expected = [ - 'sr3, shadow2, c, CAPTURING_PHASE', + 'sr3, b, c, CAPTURING_PHASE', + 'shadow2, b, c, CAPTURING_PHASE', 'sr2, b, c, CAPTURING_PHASE', 'b, b, c, AT_TARGET', 'b, b, c, AT_TARGET', 'sr2, b, c, BUBBLING_PHASE', - 'shadow2, shadow2, c, AT_TARGET', - 'shadow2, shadow2, c, AT_TARGET', - 'sr3, shadow2, c, BUBBLING_PHASE', + 'shadow2, b, c, BUBBLING_PHASE', + 'sr3, b, c, BUBBLING_PHASE', 'div, div, c, AT_TARGET', 'div, div, c, AT_TARGET', ]; @@ -881,8 +859,8 @@ test('retarget order (multiple shadow roots)', function() { c.dispatchEvent(event); var expected = [ 'div, c, div, CAPTURING_PHASE', - 'sr3, c, shadow2, CAPTURING_PHASE', - 'shadow2, c, shadow2, CAPTURING_PHASE', + 'sr3, c, b, CAPTURING_PHASE', + 'shadow2, c, b, CAPTURING_PHASE', 'sr2, c, b, CAPTURING_PHASE', 'b, c, b, CAPTURING_PHASE', 'shadow, c, b, CAPTURING_PHASE', @@ -897,8 +875,8 @@ test('retarget order (multiple shadow roots)', function() { 'shadow, c, b, BUBBLING_PHASE', 'b, c, b, BUBBLING_PHASE', 'sr2, c, b, BUBBLING_PHASE', - 'shadow2, c, shadow2, BUBBLING_PHASE', - 'sr3, c, shadow2, BUBBLING_PHASE', + 'shadow2, c, b, BUBBLING_PHASE', + 'sr3, c, b, BUBBLING_PHASE', 'div, c, div, BUBBLING_PHASE', ]; assertArrayEqual(expected, log); @@ -912,10 +890,14 @@ test('retarget order (multiple shadow roots)', function() { var event = new MouseEvent('mouseover', {relatedTarget: a, bubbles: true}); b.dispatchEvent(event); var expected = [ - 'sr2, b, shadow, CAPTURING_PHASE', - 'b, b, shadow, AT_TARGET', - 'b, b, shadow, AT_TARGET', - 'sr2, b, shadow, BUBBLING_PHASE', + 'sr3, b, a, CAPTURING_PHASE', + 'shadow2, b, a, CAPTURING_PHASE', + 'sr2, b, a, CAPTURING_PHASE', + 'b, b, a, AT_TARGET', + 'b, b, a, AT_TARGET', + 'sr2, b, a, BUBBLING_PHASE', + 'shadow2, b, a, BUBBLING_PHASE', + 'sr3, b, a, BUBBLING_PHASE', ]; assertArrayEqual(expected, log); @@ -924,16 +906,20 @@ test('retarget order (multiple shadow roots)', function() { var event = new MouseEvent('mouseover', {relatedTarget: b, bubbles: true}); a.dispatchEvent(event); var expected = [ - 'sr2, shadow, b, CAPTURING_PHASE', - 'b, shadow, b, CAPTURING_PHASE', + 'sr3, a, b, CAPTURING_PHASE', + 'shadow2, a, b, CAPTURING_PHASE', + 'sr2, a, b, CAPTURING_PHASE', + 'b, a, b, CAPTURING_PHASE', + 'shadow, a, b, CAPTURING_PHASE', 'sr, a, div, CAPTURING_PHASE', 'a, a, div, AT_TARGET', 'a, a, div, AT_TARGET', 'sr, a, div, BUBBLING_PHASE', - 'shadow, shadow, b, AT_TARGET', - 'shadow, shadow, b, AT_TARGET', - 'b, shadow, b, BUBBLING_PHASE', - 'sr2, shadow, b, BUBBLING_PHASE', + 'shadow, a, b, BUBBLING_PHASE', + 'b, a, b, BUBBLING_PHASE', + 'sr2, a, b, BUBBLING_PHASE', + 'shadow2, a, b, BUBBLING_PHASE', + 'sr3, a, b, BUBBLING_PHASE', ]; assertArrayEqual(expected, log); }); diff --git a/test/js/reprojection.js b/test/js/reprojection.js index a58b519..8985b17 100644 --- a/test/js/reprojection.js +++ b/test/js/reprojection.js @@ -102,7 +102,7 @@ suite('Shadow DOM reprojection', function() { testRender(); }); - + test('getDistributedNodes can be called before shadowRoot composition', function() { var host = document.createElement('div'); host.innerHTML = ''; diff --git a/test/js/rerender.js b/test/js/rerender.js index 5afffe9..cfc49ae 100644 --- a/test/js/rerender.js +++ b/test/js/rerender.js @@ -136,7 +136,7 @@ suite('Shadow DOM rerender', function() { var shadow = shadowRoot.firstChild; function testRender() { - assert.strictEqual(getVisualInnerHtml(host), ''); + assert.strictEqual(getVisualInnerHtml(host), ''); expectStructure(host, { firstChild: a, @@ -161,7 +161,7 @@ suite('Shadow DOM rerender', function() { testRender(); }); - test(' with fallback', function() { + test(' fallback support has been removed', function() { var host = document.createElement('div'); host.innerHTML = ''; var a = host.firstChild; @@ -172,7 +172,7 @@ suite('Shadow DOM rerender', function() { var fallback = shadow.firstChild; function testRender() { - assert.strictEqual(getVisualInnerHtml(host), 'fallback'); + assert.strictEqual(getVisualInnerHtml(host), ''); expectStructure(host, { firstChild: a, diff --git a/test/js/test.js b/test/js/test.js index 317a1ff..373304f 100644 --- a/test/js/test.js +++ b/test/js/test.js @@ -77,6 +77,7 @@ suite('Shadow DOM', function() { suite('Nested shadow roots', function() { testRender('2 levels deep', 'host', ['oldest shadow', ''], 'oldest shadow'); + testRender('4 levels deep', 'host', ['oldest shadow', '', '', ''], @@ -94,6 +95,20 @@ suite('Shadow DOM', function() { '' ], ''); + + testRender('content in shadow', + '', + [ + '', + '' + + '', + '' + + '' + + '' + + '' + + '', + ], + ''); }); suite('matches criteria', function() { @@ -178,51 +193,6 @@ suite('Shadow DOM', function() { ''); }); - suite('pseudo-class selector(s)', function() { - testRender(':link', - '', - '', - ''); - - // :visited cannot be queried in JS. - - // :target is not supported. matches(':target') does not seem to - // work in WebKit nor Firefox. - - testRender(':enabled', - '', - '', - ''); - testRender(':disabled', - '', - '', - ''); - - testRender(':checked', - '', - '', - /Firefox|MSIE 9/.test(navigator.userAgent) ? - '' : - ''); - testRender(':indeterminate', - '', - '', - '', - function(host) { - host.firstChild.indeterminate = true; - }); - - // The following are not supported. They depend on ordering. - // :nth-child() - // :nth-last-child() - // :nth-of-type() - // :nth-last-of-type() - // :first-child - // :last-child - // :first-of-type - // :last-of-type - }); - }); suite('Nested shadow hosts', function() {