Skip to content

Instantly share code, notes, and snippets.

@samthor
Created August 1, 2016 00:25
Show Gist options
  • Save samthor/9d158c2d91cc414f73d5350434cd8c8d to your computer and use it in GitHub Desktop.
Save samthor/9d158c2d91cc414f73d5350434cd8c8d to your computer and use it in GitHub Desktop.
Shadow DOM v1 polyfill experiment πŸ”§
(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;
};
}());
<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