1 | require('@webreflection/interface');
|
2 |
|
3 | const CSS_SPLITTER = /\s*,\s*/;
|
4 | const AVOID_ESCAPING = /^(?:script|style)$/i;
|
5 | const {VOID_ELEMENT, voidSanitizer} = require('./utils');
|
6 |
|
7 | const escape = require('html-escaper').escape;
|
8 | const Parser = require('htmlparser2').Parser;
|
9 | const findName = (Class, registry) => {
|
10 | for (let key in registry)
|
11 | if (registry[key] === Class)
|
12 | return key;
|
13 | };
|
14 | const 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 |
|
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 |
|
66 | const utils = require('./utils');
|
67 | const ParentNode = require('./ParentNode');
|
68 | const ChildNode = require('./ChildNode');
|
69 | const NamedNodeMap = require('./NamedNodeMap');
|
70 | const Node = require('./Node');
|
71 | const DOMTokenList = require('./DOMTokenList');
|
72 |
|
73 | function 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 |
|
81 | const 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 |
|
90 | const 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 |
|
117 | class 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 |
|
130 |
|
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 |
|
300 | module.exports = Element;
|