Created
August 1, 2016 00:25
-
-
Save samthor/9d158c2d91cc414f73d5350434cd8c8d to your computer and use it in GitHub Desktop.
Shadow DOM v1 polyfill experiment π§
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
if (Element.prototype['attachShadow']) { | |
return false; // implemented already | |
} | |
const supported = ['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'nav', 'p', 'section', 'span']; | |
/** | |
* Implements all the fake methods on Element. | |
*/ | |
class ShadowHostMix { | |
constructor(realElement) { | |
this.realElement_ = realElement; | |
this.children_ = [...realElement.childNodes]; | |
this.children_.forEach(child => realElement.removeChild(child)); | |
// TODO: These children are fancy and ours now (also ones added in appendChild). | |
// They should have assignedSlot and _fake_ parentNode properties. | |
// If they're ever removed from a fake mixin though, these should disappear/reset. | |
// Also need {previous,next}{,Element}sibling replaced. | |
} | |
get children() { | |
return this.children_.filter(e => e instanceof Element); | |
} | |
get childNodes() { | |
return this.children_.slice(); | |
} | |
get firstElementChild() { | |
return this.children[0] || null; | |
} | |
get lastElementChild() { | |
const c = this.children; | |
return c[c.length - 1] || null; | |
} | |
get childElementCount() { | |
return this.children.length; | |
} | |
append() { | |
// TODO only Firefox | |
} | |
prepend() { | |
// TODO only Firefox | |
} | |
appendChild(c) { | |
this.children_.push(c); | |
} | |
hasChildNodes() { | |
return Boolean(this.children_.length); | |
} | |
get childNodes() { | |
return this.children_.slice(); | |
} | |
insertBefore(newNode, referenceNode) { | |
// TODO | |
} | |
removeChild(child) { | |
// TODO | |
return child; | |
} | |
replaceChild(newChild, oldChild) { | |
// TODO | |
return oldChild; | |
} | |
} | |
Element.prototype['attachShadow'] = function(opts) { | |
if (opts['mode'] != 'open') { | |
throw new Error('attachShadow requires \'mode\' to be open'); | |
} | |
if (this.shadowRoot) { | |
throw new Error('already a shadow host'); | |
} | |
if (this.tagName.indexOf('-') == -1 && supported.indexOf(this.tagName.toLowerCase()) == -1) { | |
throw new Error('can\'t attach shadow root, tag not supported'); | |
} | |
// Element is a Node, EventTarget, ParentNode, ChildNode, and others but we don't care. | |
const mix = new ShadowHostMix(this); | |
const frag = document.createDocumentFragment(); | |
const real = this; // in cases where 'this' breaks | |
// Set up the fragment so it actually calls the real elenment. | |
// Set up the real element so it actually calls the mixin. | |
Object.getOwnPropertyNames(mix.__proto__).forEach(prop => { | |
if (prop == 'constructor') { return; } | |
const desc = Object.getOwnPropertyDescriptor(mix.__proto__, prop); | |
if ('set' in desc) { | |
// TODO: how do we use 'real' getters? | |
// Object.defineProperty(frag, prop, { get: function() { | |
// return this[prop]; | |
// }, configurable: false }); | |
Object.defineProperty(this, prop, { get: function() { | |
return mix[prop]; | |
}, configurable: false }); | |
} else if (this[prop]) { | |
Object.defineProperty(frag, prop, { value: this[prop].bind(this), configurable: false }); | |
Object.defineProperty(this, prop, { value: mix[prop].bind(mix), configurable: false }); | |
} | |
}); | |
// TODO: textContent/innerHTML/innerText | |
Object.defineProperty(this, 'shadowRoot', { value: frag, configurable: false }); | |
Object.defineProperty(frag, 'host', { value: this, configurable: false }); | |
function updateSlot(slot) { | |
console.info('got slot', slot); | |
mix.children_.forEach(child => { | |
slot.appendChild(child); | |
// TODO: see comment in mixin | |
Object.defineProperty(child, 'assignedSlot', { value: slot }); | |
Object.defineProperty(child, 'parentNode', { value: real }); | |
}); | |
} | |
const mo = new MutationObserver(mutations => { | |
mutations.forEach(record => { | |
record.removedNodes.forEach(removed => { | |
if (removed.assignedSlot) { | |
// TODO: fix parentNode | |
console.warn('need to clear fake parentNode/assignedSlot', removed); | |
Object.defineProperty(removed, 'assignedSlot', { value: null }); | |
Object.defineProperty(removed, 'parentNode', { value: null }); | |
} | |
}); | |
record.addedNodes.forEach(added => { | |
if (added.tagName && added.tagName.toLowerCase() == 'slot') { | |
updateSlot(added); | |
} | |
}); | |
}); | |
}); | |
mo.observe(this, { childList: true, subtree: true }); | |
return frag; | |
}; | |
}()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="script.js"></script> | |
<div id="test"> | |
Hello! I should be placed somewhere else. | |
</div> | |
<script> | |
var t = test.childNodes[0]; | |
var shadowRoot = test.attachShadow({'mode': 'open'}); | |
var div = document.createElement('div'); | |
div.textContent = 'random div content'; | |
shadowRoot.appendChild(div); | |
var slot = document.createElement('slot'); | |
shadowRoot.appendChild(slot); | |
console.info('parentNode', t.parentNode, 'assignedSlot', t.assignedSlot); | |
// t.remove(); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment