1 | require('undom/register');
|
2 | const { parseFragment } = require('parse5');
|
3 |
|
4 | const ElementProto = Element.prototype;
|
5 | const NodeProto = Node.prototype;
|
6 | const { insertBefore, removeChild } = NodeProto;
|
7 | const { dispatchEvent } = ElementProto;
|
8 | const { defineProperty: prop } = Object;
|
9 | const isConnected = Symbol('isConnected');
|
10 |
|
11 | function expose (name, value) {
|
12 | global[name] = window[name] = value;
|
13 | return value;
|
14 | }
|
15 |
|
16 | function connectNode (node) {
|
17 | if (node.connectedCallback && !node[isConnected]) {
|
18 | node.connectedCallback();
|
19 | }
|
20 | node[isConnected] = true;
|
21 | }
|
22 |
|
23 | function disconnectNode (node) {
|
24 | if (node.disconnectedCallback && node[isConnected]) {
|
25 | node.disconnectedCallback();
|
26 | }
|
27 | node[isConnected] = false;
|
28 | }
|
29 |
|
30 | function 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 |
|
39 | function 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 |
|
59 | function 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 |
|
80 | function patchDocumentFragment () {
|
81 | expose('DocumentFragment', class extends Node {
|
82 | get nodeName () {
|
83 | return '#document-fragment';
|
84 | }
|
85 | });
|
86 | document.createDocumentFragment = () => new DocumentFragment();
|
87 | }
|
88 |
|
89 | function 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 |
|
162 | function 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 |
|
183 | function 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 |
|
202 | function 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 |
|
233 | NodeProto.insertBefore = function (newNode, refNode) {
|
234 | return each(newNode, newNode => {
|
235 | insertBefore.call(this, newNode, refNode);
|
236 | connectNode(newNode);
|
237 | });
|
238 | };
|
239 |
|
240 |
|
241 | NodeProto.removeChild = function (refNode) {
|
242 | return each(refNode, refNode => {
|
243 | removeChild.call(this, refNode);
|
244 | disconnectNode(refNode);
|
245 | });
|
246 | };
|
247 | }
|
248 |
|
249 | patchCustomElements();
|
250 | patchDocumentFragment();
|
251 | patchElement();
|
252 | patchEvents();
|
253 | patchHTMLElement();
|
254 | patchNode();
|