UNPKG

7.77 kBJavaScriptView Raw
1require('@webreflection/interface');
2
3const CSS_SPLITTER = /\s*,\s*/;
4const AVOID_ESCAPING = /^(?:script|style)$/i;
5const {VOID_ELEMENT, voidSanitizer} = require('./utils');
6
7const escape = require('html-escaper').escape;
8const Parser = require('htmlparser2').Parser;
9const findName = (Class, registry) => {
10 for (let key in registry)
11 if (registry[key] === Class)
12 return key;
13};
14const parseInto = (node, html) => {
15 const stack = [];
16 const document = node.ownerDocument;
17 const content = new Parser({
18 onopentagname(name) {
19 switch (name) {
20 case 'html': break;
21 case 'head':
22 case 'body':
23 node.replaceChild(document.createElement(name), document[name]);
24 node = document[name];
25 break;
26 default:
27 const child = document.createElement(name);
28 if (child.isCustomElement) {
29 stack.push(node, child);
30 node = child;
31 }
32 else
33 node = node.appendChild(child);
34 break;
35 }
36 },
37 onattribute(name, value) {
38 node.setAttribute(name, value);
39 },
40 oncomment(data) {
41 node.appendChild(document.createComment(data));
42 },
43 ontext(text) {
44 node.appendChild(document.createTextNode(text));
45 },
46 onclosetag(name) {
47 switch (name) {
48 case 'html': break;
49 default:
50 while (stack.length)
51 stack.shift().appendChild(stack.shift());
52 /* istanbul ignore else */
53 if (node.nodeName === name)
54 node = node.parentNode;
55 break;
56 }
57 }
58 }, {
59 decodeEntities: true,
60 xmlMode: true
61 });
62 content.write(voidSanitizer(html));
63 content.end();
64};
65
66const utils = require('./utils');
67const ParentNode = require('./ParentNode');
68const ChildNode = require('./ChildNode');
69const NamedNodeMap = require('./NamedNodeMap');
70const Node = require('./Node');
71const DOMTokenList = require('./DOMTokenList');
72
73function matchesBySelector(css) {
74 switch (css[0]) {
75 case '#': return this.id === css.slice(1);
76 case '.': return this.classList.contains(css.slice(1));
77 default: return css === this.nodeName;
78 }
79}
80
81const specialAttribute = (owner, attr) => {
82 switch (attr.name) {
83 case 'class':
84 owner.classList.value = attr.value;
85 return true;
86 }
87 return false;
88};
89
90const stringifiedNode = el => {
91 switch (el.nodeType) {
92 case Node.ELEMENT_NODE:
93 return ('<' + el.nodeName).concat(
94 el.attributes.map(stringifiedNode).join(''),
95 VOID_ELEMENT.test(el.nodeName) ?
96 ' />' :
97 ('>' + (
98 AVOID_ESCAPING.test(el.nodeName) ?
99 el.textContent :
100 el.childNodes.map(stringifiedNode).join('')
101 ) + '</' + el.nodeName + '>')
102 );
103 case Node.ATTRIBUTE_NODE:
104 return el.name === 'style' && !el.value ? '' : (
105 typeof el.value === 'boolean' || el.value == null ?
106 (el.value ? (' ' + el.name) : '') :
107 (' ' + el.name + '="' + escape(el.value) + '"')
108 );
109 case Node.TEXT_NODE:
110 return escape(el.data);
111 case Node.COMMENT_NODE:
112 return '<!--' + el.data + '-->';
113 }
114};
115
116// interface Element // https://dom.spec.whatwg.org/#interface-element
117class Element extends Node.implements(ParentNode, ChildNode) {
118 constructor(ownerDocument, name) {
119 super(ownerDocument);
120 this.attributes = new NamedNodeMap(this);
121 this.nodeType = Node.ELEMENT_NODE;
122 this.nodeName = name || findName(
123 this.constructor,
124 this.ownerDocument.customElements._registry
125 );
126 this.classList = new DOMTokenList(this);
127 }
128
129 // it doesn't actually really work as expected
130 // it simply provides shadowRoot as the element itself
131 attachShadow(init) {
132 switch (init.mode) {
133 case 'open': return (this.shadowRoot = this);
134 case 'closed': return this;
135 }
136 throw new Error('element.attachShadow({mode: "open" | "closed"})');
137 }
138
139 getAttribute(name) {
140 const attr = this.getAttributeNode(name);
141 return attr && attr.value;
142 }
143
144 getAttributeNames() {
145 return this.attributes.map(attr => attr.name);
146 }
147
148 getAttributeNode(name) {
149 return this.attributes.find(attr => attr.name === name) || null;
150 }
151
152 getElementsByClassName(name) {
153 const list = [];
154 for (let i = 0; i < this.children.length; i++) {
155 let el = this.children[i];
156 if (el.classList.contains(name)) list.push(el);
157 list.push(...el.getElementsByClassName(name));
158 }
159 return list;
160 }
161
162 getElementsByTagName(name) {
163 const list = [];
164 for (let i = 0; i < this.children.length; i++) {
165 let el = this.children[i];
166 if (name === '*' || el.nodeName === name) list.push(el);
167 list.push(...el.getElementsByTagName(name));
168 }
169 return list;
170 }
171
172 hasAttribute(name) {
173 return this.attributes.some(attr => attr.name === name);
174 }
175
176 hasAttributes() {
177 return 0 < this.attributes.length;
178 }
179
180 closest(css) {
181 let el = this;
182 do {
183 if (el.matches(css)) return el;
184 } while ((el = el.parentNode) && el.nodeType === Node.ELEMENT_NODE);
185 return null;
186 }
187
188 matches(css) {
189 return css.split(CSS_SPLITTER).some(matchesBySelector, this);
190 }
191
192 removeAttribute(name) {
193 const attr = this.getAttributeNode(name);
194 if (attr) this.removeAttributeNode(attr);
195 }
196
197 setAttribute(name, value) {
198 const attr = this.getAttributeNode(name);
199 if (attr) {
200 attr.value = value;
201 } else {
202 const attr = this.ownerDocument.createAttribute(name);
203 attr.ownerElement = this;
204 this.attributes.push(attr);
205 this.attributes[name] = attr;
206 attr.value = value;
207 }
208 }
209
210 removeAttributeNode(attr) {
211 const i = this.attributes.indexOf(attr);
212 if (i < 0) throw new Error('unable to remove ' + attr);
213 this.attributes.splice(i, 1);
214 attr.value = null;
215 delete this.attributes[attr.name];
216 specialAttribute(this, attr);
217 }
218
219 removeAttributeNodeNS(attr) {
220 return this.removeAttributeNode(attr);
221 }
222
223 setAttributeNode(attr) {
224 const name = attr.name;
225 const old = this.getAttributeNode(name);
226 if (old === attr) return attr;
227 else {
228 if (attr.ownerElement) {
229 if (attr.ownerElement !== this) {
230 throw new Error('The attribute is already used in other nodes.');
231 }
232 }
233 else attr.ownerElement = this;
234 this.attributes[name] = attr;
235 if (old) {
236 this.attributes.splice(this.attributes.indexOf(old), 1, attr);
237 if (!specialAttribute(this, attr))
238 utils.notifyAttributeChanged(this, name, old.value, attr.value);
239 return old;
240 } else {
241 this.attributes.push(attr);
242 if (!specialAttribute(this, attr))
243 utils.notifyAttributeChanged(this, name, null, attr.value);
244 return null;
245 }
246 }
247 }
248
249 setAttributeNodeNS(attr) {
250 return this.setAttribute(attr);
251 }
252
253 get id() {
254 return this.getAttribute('id') || '';
255 }
256
257 set id(value) {
258 this.setAttribute('id', value);
259 }
260
261 get className() {
262 return this.classList.value;
263 }
264
265 set className(value) {
266 this.classList.value = value;
267 }
268
269 get innerHTML() {
270 return this.childNodes.map(stringifiedNode).join('');
271 }
272
273 set innerHTML(html) {
274 this.textContent = '';
275 parseInto(this, html);
276 }
277
278 get nextElementSibling() {
279 const children = this.parentNode.children;
280 let i = children.indexOf(this);
281 return ++i < children.length ? children[i] : null;
282 }
283
284 get previousElementSibling() {
285 const children = this.parentNode.children;
286 let i = children.indexOf(this);
287 return --i < 0 ? null : children[i];
288 }
289
290 get outerHTML() {
291 return stringifiedNode(this);
292 }
293
294 get tagName() {
295 return this.nodeName
296 }
297
298};
299
300module.exports = Element;