UNPKG

6.9 kBJavaScriptView Raw
1require('undom/register');
2const { parseFragment } = require('parse5');
3
4const ElementProto = Element.prototype;
5const NodeProto = Node.prototype;
6const { insertBefore, removeChild } = NodeProto;
7const { dispatchEvent } = ElementProto;
8const { defineProperty: prop } = Object;
9const isConnected = Symbol('isConnected');
10
11function expose (name, value) {
12 global[name] = window[name] = value;
13 return value;
14}
15
16function connectNode (node) {
17 if (node.connectedCallback && !node[isConnected]) {
18 node.connectedCallback();
19 }
20 node[isConnected] = true;
21}
22
23function disconnectNode (node) {
24 if (node.disconnectedCallback && node[isConnected]) {
25 node.disconnectedCallback();
26 }
27 node[isConnected] = false;
28}
29
30function each (node, call) {
31 if (node instanceof DocumentFragment) {
32 Array.from(node.childNodes).forEach(call);
33 } else {
34 call(node);
35 }
36 return node;
37}
38
39function translateParsed (parsed) {
40 let node;
41 const { attrs, childNodes, nodeName, value } = parsed;
42
43 if (nodeName === '#document-fragment') {
44 node = document.createDocumentFragment();
45 } else if (nodeName === '#text') {
46 node = document.createTextNode(value);
47 } else {
48 node = document.createElement(nodeName);
49 attrs.forEach(({ name, value }) => node.setAttribute(name, value));
50 }
51
52 if (childNodes) {
53 childNodes.forEach(c => node.appendChild(translateParsed(c)));
54 }
55
56 return node;
57}
58
59function patchCustomElements () {
60 const customElementRegistry = {};
61 expose('customElements', {
62 define (name, func) {
63 prop(func.prototype, 'nodeName', { value: name.toUpperCase() });
64 customElementRegistry[name] = func;
65 },
66 get (name) {
67 return customElementRegistry[name];
68 }
69 });
70
71 const createElement = document.createElement.bind(document);
72 document.createElement = function (name) {
73 const Ctor = window.customElements.get(name);
74 const elem = Ctor ? new Ctor() : createElement(name);
75 prop(elem, 'localName', { value: name });
76 return elem;
77 };
78}
79
80function patchDocumentFragment () {
81 expose('DocumentFragment', class extends Node {
82 get nodeName () {
83 return '#document-fragment';
84 }
85 });
86 document.createDocumentFragment = () => new DocumentFragment();
87}
88
89function patchElement () {
90 const { getAttribute, removeAttribute, setAttribute } = ElementProto;
91 ElementProto.dispatchEvent = function (evnt) {
92 evnt.target = this;
93 return dispatchEvent.call(this, evnt);
94 };
95 ElementProto.getAttribute = function (name) {
96 const value = getAttribute.call(this, name);
97 return value == null ? null : value;
98 };
99 ElementProto.hasAttribute = function (name) {
100 return this.getAttribute(name) !== null;
101 };
102 ElementProto.removeAttribute = function (name) {
103 const oldValue = this.getAttribute(name);
104 removeAttribute.call(this, name);
105 if (this.attributeChangedCallback) {
106 this.attributeChangedCallback(name, oldValue, null);
107 }
108 };
109 ElementProto.setAttribute = function (name, newValue) {
110 const oldValue = this.getAttribute(name);
111 setAttribute.call(this, name, newValue);
112 if (this.attributeChangedCallback) {
113 this.attributeChangedCallback(name, oldValue, newValue);
114 }
115 };
116 ElementProto.assignedNodes = function () {
117 if (this.nodeName !== 'SLOT') {
118 throw new Error('Non-standard: assignedNodes() called on non-slot element.');
119 }
120
121 const name = this.getAttribute('name') || this.name;
122
123 let node = this, host;
124 while (node = node.parentNode) {
125 if (node.host) {
126 host = node.host;
127 break;
128 }
129 }
130
131 return host
132 ? host.childNodes.filter(n => {
133 return name
134 ? n.getAttribute && n.getAttribute('slot') === name
135 : !n.getAttribute || !n.getAttribute('slot')
136 })
137 : [];
138 };
139 prop(ElementProto, 'innerHTML', {
140 get () {
141 return this.childNodes.map(c => c.outerHTML || c.textContent).join('');
142 },
143 set (val) {
144 while (this.hasChildNodes()) {
145 this.removeChild(this.firstChild);
146 }
147 this.appendChild(translateParsed(parseFragment(val)));
148 }
149 });
150 prop(ElementProto, 'outerHTML', {
151 get () {
152 const { attributes, nodeName } = this;
153 const name = nodeName.toLowerCase();
154 return `<${name}${attributes.reduce((prev, { name, value }) => prev + ` ${name}="${value}"`, '')}>${this.innerHTML}</${name}>`;
155 },
156 set (val) {
157 throw new Error('Not implemented: set outerHTML');
158 }
159 });
160}
161
162function patchEvents () {
163 expose('CustomEvent', expose('Event', class extends Event {
164 constructor (evnt, opts = {}) {
165 super(evnt, opts);
166 }
167 initEvent (type, bubbles, cancelable) {
168 this.bubbles = bubbles;
169 this.cancelable = cancelable;
170 this.type = type;
171 }
172 initCustomEvent (type, bubbles, cancelable, detail) {
173 this.initEvent(type, bubbles, cancelable);
174 this.detail = detail;
175 }
176 }));
177
178 document.createEvent = function (name) {
179 return new window[name]();
180 };
181}
182
183function patchHTMLElement () {
184 function HTMLElement() {
185 let newTarget = this.constructor;
186 return Reflect.construct(Element, [], newTarget);
187 }
188 HTMLElement.prototype = Object.create(Element.prototype, {
189 constructor: { value: HTMLElement, configurable: true, writable: true }
190 });
191 HTMLElement.prototype.attachShadow = function({ mode }) {
192 const shadowRoot = document.createElement('shadow-root');
193 prop(this, 'shadowRoot', { value: shadowRoot });
194 prop(shadowRoot, 'host', { value: this });
195 prop(shadowRoot, 'mode', { value: mode });
196 prop(shadowRoot, 'parentNode', { value: this });
197 return shadowRoot;
198 };
199 expose('HTMLElement', HTMLElement);
200}
201
202function patchNode () {
203 Node.DOCUMENT_FRAGMENT_NODE = 11;
204 Node.ELEMENT_NODE = 1;
205 Node.TEXT_NODE = 3;
206
207 prop(NodeProto, 'textContent', {
208 get () {
209 return this.childNodes.map(c => c.nodeValue).join('');
210 },
211 set (val) {
212 this.appendChild(document.createTextNode(val));
213 }
214 });
215
216 NodeProto.contains = function (node) {
217 if (this === node) {
218 return true;
219 }
220 for (let childNode of this.childNodes) {
221 if (childNode.contains(node)) {
222 return true;
223 }
224 }
225 return false;
226 };
227
228 NodeProto.hasChildNodes = function () {
229 return this.childNodes.length;
230 };
231
232 // Undom internally calls insertBefore in appendChild.
233 NodeProto.insertBefore = function (newNode, refNode) {
234 return each(newNode, newNode => {
235 insertBefore.call(this, newNode, refNode);
236 connectNode(newNode);
237 });
238 };
239
240 // Undom internally calls removeChild in replaceChild.
241 NodeProto.removeChild = function (refNode) {
242 return each(refNode, refNode => {
243 removeChild.call(this, refNode);
244 disconnectNode(refNode);
245 });
246 };
247}
248
249patchCustomElements();
250patchDocumentFragment();
251patchElement();
252patchEvents();
253patchHTMLElement();
254patchNode();