-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
document.blockingElements polyfill with inert #1
Changes from 27 commits
2ee1dc9
1a58d42
65d9f44
01bf3bd
8a0346a
ff302a6
526ebb5
d0f4dbf
02dd4bc
8577af6
d6f7ac3
7947475
81f054e
b9ba9b8
55a48e6
b29db53
9f964aa
23a905e
3c9cccc
d1fee38
feaf2be
fd5769c
3541b98
f53dbc8
4949ca8
3312844
b3ce161
410ac57
cf89f3f
9e267e4
8b1651a
db8a53a
e4d46d5
1a33482
3fcc78c
c8e0120
19178ea
601b323
0dcb7da
9c0d667
d9b10ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
node_modules | ||
bower_components |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
language: node_js | ||
node_js: stable | ||
dist: trusty | ||
sudo: required | ||
addons: | ||
firefox: latest | ||
apt: | ||
sources: | ||
- google-chrome | ||
packages: | ||
- google-chrome-stable | ||
before_script: | ||
- npm install -g bower polylint web-component-tester | ||
- bower install | ||
- polylint | ||
script: xvfb-run wct |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<script src="blocking-elements.js"></script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,325 @@ | ||
/** | ||
* | ||
* Copyright 2016 Google Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
(function(document) { | ||
|
||
/* Symbols for blocking elements and already inert elements */ | ||
const BLOCKING_ELEMS = Symbol('blockingElements'); | ||
const ALREADY_INERT_ELEMS = Symbol('alreadyInertElements'); | ||
|
||
/* Symbols for static methods */ | ||
const TOP_CHANGED_FN = Symbol('topChanged'); | ||
const NOT_INERTABLE_FN = Symbol('notInertable'); | ||
const SET_SIBLINGS_INERT_FN = Symbol('setInertToSiblingsOfElement'); | ||
const GET_PARENTS_FN = Symbol('getParents'); | ||
const GET_DISTRIB_CHILDREN_FN = Symbol('getDistributedChildren'); | ||
const IS_INERT_FN = Symbol('isInert'); | ||
const SET_INERT_FN = Symbol('setInert'); | ||
|
||
/** | ||
* `BlockingElements` manages a stack of elements that inert the interaction | ||
* outside them. The top element is the interactive part of the document. | ||
* The stack can be updated with the methods `push, remove, pop`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should start using @class annotations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, they're redundant for Closure. You don't need them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that documented somewhere? |
||
* @class | ||
*/ | ||
class BlockingElements { | ||
constructor() { | ||
/** | ||
* The blocking elements. | ||
* @type {Array<HTMLElement>} | ||
* @private | ||
*/ | ||
this[BLOCKING_ELEMS] = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be used like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main reason is to be able to get the top element more easily, without converting the Set into an array each time. With a Set, I'd have to do something like function getLastValue(set){
var value;
for(value of set);
return value;
} WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can always subclass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ended up converting it to a |
||
|
||
/** | ||
* Elements that are already inert before the first blocking element is pushed. | ||
* @type {Set<HTMLElement>} | ||
* @private | ||
*/ | ||
this[ALREADY_INERT_ELEMS] = new Set(); | ||
} | ||
|
||
/** | ||
* Call this whenever this object is about to become obsolete. This empties | ||
* the blocking elements | ||
*/ | ||
destructor() { | ||
// Loop from the last to first to gradually update the tree up to body. | ||
const elems = this[BLOCKING_ELEMS]; | ||
for (let i = elems.length - 1; i >= 0; i--) { | ||
BlockingElements[TOP_CHANGED_FN](elems[i - 1], elems[i], this[ALREADY_INERT_ELEMS]); | ||
} | ||
delete this[BLOCKING_ELEMS]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. avoid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, done! 👌 |
||
delete this[ALREADY_INERT_ELEMS]; | ||
} | ||
|
||
/** | ||
* A copy of the blocking elements. | ||
* @type {Array<HTMLElement>} | ||
*/ | ||
get all() { | ||
return Array.prototype.slice.call(this[BLOCKING_ELEMS]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but this getter should return a copy of it, since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but this is exactly the same as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ..touché! I avoided There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could also return an iterator, which has no API for modification, and let the caller decide whether to clone into a new Array: A caller can directly use that in a for/of loop, or with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah.... IE. Your'e compiling this though, yes? You can look into runtime support from Closure. |
||
} | ||
|
||
/** | ||
* The top blocking element. | ||
* @type {HTMLElement|null} | ||
*/ | ||
get top() { | ||
const elems = this[BLOCKING_ELEMS]; | ||
return elems[elems.length - 1] || null; | ||
} | ||
|
||
/** | ||
* Adds the element to the blocking elements. | ||
* @param {!HTMLElement} element | ||
*/ | ||
push(element) { | ||
const i = this[BLOCKING_ELEMS].indexOf(element); | ||
if (i !== -1) { | ||
console.warn('element already added in document.blockingElements'); | ||
return; | ||
} | ||
const oldTop = this.top; | ||
this[BLOCKING_ELEMS].push(element); | ||
BlockingElements[TOP_CHANGED_FN](element, oldTop, this[ALREADY_INERT_ELEMS]); | ||
} | ||
|
||
/** | ||
* Removes the element from the blocking elements. | ||
* @param {!HTMLElement} element | ||
*/ | ||
remove(element) { | ||
const i = this[BLOCKING_ELEMS].indexOf(element); | ||
if (i !== -1) { | ||
this[BLOCKING_ELEMS].splice(i, 1); | ||
// Top changed only if the removed element was the top element. | ||
if (i === this[BLOCKING_ELEMS].length) { | ||
BlockingElements[TOP_CHANGED_FN](this.top, element, this[ALREADY_INERT_ELEMS]); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Remove the top blocking element and returns it. | ||
* @returns {HTMLElement|null} the removed element. | ||
*/ | ||
pop() { | ||
const top = this.top; | ||
top && this.remove(top); | ||
return top; | ||
} | ||
|
||
/** | ||
* Sets `inert` to all document elements except the new top element, its parents, | ||
* and its distributed content. Pass `oldTop` to limit element updates (will look | ||
* for common parents and avoid setting them twice). | ||
* When the first blocking element is added (`newTop = null`), it saves the elements | ||
* that are already inert into `alreadyInertElems`. When the last blocking element | ||
* is removed (`oldTop = null`), `alreadyInertElems` are kept inert. | ||
* @param {HTMLElement} newTop If null, it means the last blocking element was removed. | ||
* @param {HTMLElement} oldTop If null, it means the first blocking element was added. | ||
* @param {!Set<HTMLElement>} alreadyInertElems Elements to be kept inert. | ||
* @private | ||
*/ | ||
static[TOP_CHANGED_FN](newTop, oldTop, alreadyInertElems) { | ||
const oldElParents = oldTop ? this[GET_PARENTS_FN](oldTop) : []; | ||
const newElParents = newTop ? this[GET_PARENTS_FN](newTop) : []; | ||
const elemsToSkip = newTop && newTop.shadowRoot ? | ||
this[GET_DISTRIB_CHILDREN_FN](newTop.shadowRoot) : null; | ||
// Loop from top to deepest elements, so we find the common parents and | ||
// avoid setting them twice. | ||
while (oldElParents.length || newElParents.length) { | ||
const oldElParent = oldElParents.pop(); | ||
const newElParent = newElParents.pop(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tools team is leaning in the direction of only using At the risk of starting a 🔥 /cc @justinfagnani @rictic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaning, pending final disposition of a gentlemanly duel of ideas between the tools team :) I personally prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. synced offline, and the agreement is to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
if (oldElParent === newElParent) { | ||
continue; | ||
} | ||
// Same parent, set only these 2 children. | ||
if (oldElParent && newElParent && | ||
oldElParent.parentNode === newElParent.parentNode) { | ||
if (!oldTop && this[IS_INERT_FN](oldElParent)) { | ||
alreadyInertElems.add(oldElParent); | ||
} | ||
this[SET_INERT_FN](oldElParent, true); | ||
this[SET_INERT_FN](newElParent, alreadyInertElems.has(newElParent)); | ||
} else { | ||
oldElParent && this[SET_SIBLINGS_INERT_FN](oldElParent, false, elemsToSkip, | ||
alreadyInertElems); | ||
// Collect the already inert elements only if it is the first blocking | ||
// element (if oldTop = null) | ||
newElParent && this[SET_SIBLINGS_INERT_FN](newElParent, true, elemsToSkip, | ||
oldTop ? null : alreadyInertElems); | ||
} | ||
} | ||
if (!newTop) { | ||
alreadyInertElems.clear(); | ||
} | ||
} | ||
|
||
/** | ||
* Returns if the element is not inertable. | ||
* @param {!HTMLElement} element | ||
* @returns {boolean} | ||
* @private | ||
*/ | ||
static[NOT_INERTABLE_FN](element) { | ||
return /^(style|template|script)$/.test(element.localName); | ||
} | ||
|
||
/** | ||
* Sets `inert` to the siblings of the element except the elements to skip. | ||
* If `inert = true`, already inert elements are added into `alreadyInertElems`. | ||
* If `inert = false`, siblings that are contained in `alreadyInertElems` will | ||
* be kept inert. | ||
* @param {!HTMLElement} element | ||
* @param {boolean} inert | ||
* @param {Set<HTMLElement>} elemsToSkip | ||
* @param {Set<HTMLElement>} alreadyInertElems | ||
* @private | ||
*/ | ||
static[SET_SIBLINGS_INERT_FN](element, inert, elemsToSkip, alreadyInertElems) { | ||
// Previous siblings. | ||
let sibling = element; | ||
while ((sibling = sibling.previousElementSibling)) { | ||
// If not inertable or to be skipped, skip. | ||
if (this[NOT_INERTABLE_FN](sibling) || (elemsToSkip && elemsToSkip.has(sibling))) { | ||
continue; | ||
} | ||
// Should be collected since already inerted. | ||
if (alreadyInertElems && inert && this[IS_INERT_FN](sibling)) { | ||
alreadyInertElems.add(sibling); | ||
} | ||
// Should be kept inert if it's in `alreadyInertElems`. | ||
this[SET_INERT_FN](sibling, inert || (alreadyInertElems && alreadyInertElems.has(sibling))); | ||
} | ||
// Next siblings. | ||
sibling = element; | ||
while ((sibling = sibling.nextElementSibling)) { | ||
// If not inertable or to be skipped, skip. | ||
if (this[NOT_INERTABLE_FN](sibling) || (elemsToSkip && elemsToSkip.has(sibling))) { | ||
continue; | ||
} | ||
// Should be collected since already inerted. | ||
if (alreadyInertElems && inert && this[IS_INERT_FN](sibling)) { | ||
alreadyInertElems.add(sibling); | ||
} | ||
// Should be kept inert if it's in `alreadyInertElems`. | ||
this[SET_INERT_FN](sibling, inert || (alreadyInertElems && alreadyInertElems.has(sibling))); | ||
} | ||
} | ||
|
||
/** | ||
* Returns the list of parents of an element, starting from element (included) | ||
* up to `document.body` (excluded). | ||
* @param {!HTMLElement} element | ||
* @returns {Array<HTMLElement>} | ||
* @private | ||
*/ | ||
static[GET_PARENTS_FN](element) { | ||
const parents = []; | ||
let current = element; | ||
// Stop to body. | ||
while (current && current !== document.body) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The syntax for this loop might be able to be tightened up a bit? let current = element;
do {
// ...
} while (current = current.parentNode || current.host) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, there are multiple conditions, so maybe my proposal would just be uglier.. |
||
// Skip shadow roots. | ||
if (current.nodeType === Node.ELEMENT_NODE) { | ||
parents.push(current); | ||
} | ||
// ShadowDom v1 | ||
if (current.assignedSlot) { | ||
// Collect slots from deepest slot to top. | ||
while ((current = current.assignedSlot)) { | ||
parents.push(current); | ||
} | ||
// Continue the search on the top slot. | ||
current = parents.pop(); | ||
continue; | ||
} | ||
// ShadowDom v0 | ||
const insertionPoints = current.getDestinationInsertionPoints ? | ||
current.getDestinationInsertionPoints() : []; | ||
if (insertionPoints.length) { | ||
for (let i = 0; i < insertionPoints.length - 1; i++) { | ||
parents.push(current); | ||
} | ||
// Continue the search on the top content. | ||
current = insertionPoints[insertionPoints.length - 1]; | ||
continue; | ||
} | ||
current = current.parentNode || current.host; | ||
} | ||
return parents; | ||
} | ||
|
||
/** | ||
* Returns the distributed children of a shadow root. | ||
* @param {!DocumentFragment} shadowRoot | ||
* @returns {Set<HTMLElement>} | ||
* @private | ||
*/ | ||
static[GET_DISTRIB_CHILDREN_FN](shadowRoot) { | ||
const result = new Set(); | ||
// ShadowDom v1 | ||
const slots = shadowRoot.querySelectorAll('slot'); | ||
for (let i = 0; i < slots.length; i++) { | ||
const nodes = slots[i].assignedNodes({ | ||
flatten: true | ||
}); | ||
for (let j = 0; j < nodes.length; j++) { | ||
if (nodes[j].nodeType === Node.ELEMENT_NODE) { | ||
result.add(nodes[j]); | ||
} | ||
} | ||
} | ||
// ShadowDom v0 | ||
const contents = shadowRoot.querySelectorAll('content'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to skip this check entirely if you already found slots in this shadow root? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In an hybrid scenario where we both have "working" and and their |
||
for (let i = 0; i < contents.length; i++) { | ||
const nodes = contents[i].getDistributedNodes(); | ||
for (let j = 0; j < nodes.length; j++) { | ||
if (nodes[j].nodeType === Node.ELEMENT_NODE) { | ||
result.add(nodes[j]); | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Returns if an element is inert. | ||
* @param {!HTMLElement} element | ||
* @returns {boolean} | ||
* @private | ||
*/ | ||
static[IS_INERT_FN](element) { | ||
return !!element.inert; | ||
} | ||
|
||
/** | ||
* Sets inert to an element. | ||
* @param {!HTMLElement} element | ||
* @param {boolean} inert | ||
* @private | ||
*/ | ||
static[SET_INERT_FN](element, inert) { | ||
// Update JS property. | ||
element.inert = inert; | ||
} | ||
} | ||
|
||
document.$blockingElements = new BlockingElements(); | ||
|
||
})(document); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "blockingElements", | ||
"description": "A polyfill for the proposed blocking elments stack API", | ||
"main": "blocking-elements.html", | ||
"authors": [ | ||
"Valdrin Koshi <[email protected]>" | ||
], | ||
"license": "Apache-2.0", | ||
"keywords": [ | ||
"blocking", | ||
"elements", | ||
"polyfill", | ||
"browser" | ||
], | ||
"homepage": "https://github.com/PolymerLabs/blockingElements", | ||
"private": true, | ||
"ignore": [ | ||
"**/.*", | ||
"node_modules", | ||
"bower_components", | ||
"test", | ||
"tests" | ||
], | ||
"devDependencies": { | ||
"test-fixture": "PolymerElements/test-fixture#ce-v1", | ||
"inert": "WICG/inert#master", | ||
"web-component-tester": "^4.0.0", | ||
"webcomponentsjs": "webcomponents/webcomponentsjs#v1-polymer-edits" | ||
}, | ||
"resolutions": { | ||
"test-fixture": "ce-v1" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI and in case you care, I'm going to advocate in tools that for Symbols we don't use
SCREAMING_CASE
, but_privateCamelCase
names. I think it'll read better in the class bodies, and I also in general disagree with all caps forconst
, especially ifconst
is the default.compare:
and
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it's less of a punch in the eye! Will update :)