UNPKG

21.1 kBJavaScriptView Raw
1"use strict";
2
3const {Event} = require("./Events");
4const DocumentFragment = require("./DocumentFragment");
5const DOMException = require("domexception");
6const makeAbsolute = require("./makeAbsolute");
7const vm = require("vm");
8const {EventEmitter} = require("events");
9
10const rwProperties = ["id", "name", "type"];
11const inputElements = ["input", "button", "textarea"];
12
13module.exports = Element;
14
15function Element(document, $elm) {
16 const {$, location, _getElement} = document;
17 const tagName = (($elm[0] && $elm[0].name) || "").toLowerCase();
18
19 const rects = {
20 top: 99999,
21 bottom: 99999,
22 right: 0,
23 left: 0,
24 height: 0,
25 width: 0
26 };
27
28 rects.bottom = rects.top + rects.height;
29
30 const emitter = new EventEmitter();
31
32 const classList = getClassList();
33
34 const element = {
35 $elm,
36 _emitter: emitter,
37 _setBoundingClientRect: setBoundingClientRect,
38 appendChild,
39 classList,
40 click,
41 closest,
42 contains,
43 dispatchEvent,
44 getAttribute,
45 getBoundingClientRect,
46 getElementsByClassName,
47 getElementsByTagName,
48 hasAttribute,
49 insertAdjacentHTML,
50 insertBefore,
51 matches,
52 remove,
53 removeAttribute,
54 removeChild,
55 requestFullscreen,
56 setAttribute,
57 setElementsToScroll,
58 cloneNode,
59 style: getStyle(),
60 };
61
62 Object.setPrototypeOf(element, Element.prototype);
63
64 Object.defineProperty(element, "tagName", {
65 get: () => tagName ? tagName.toUpperCase() : undefined
66 });
67
68 rwProperties.forEach((p) => {
69 Object.defineProperty(element, p, {
70 get: () => getAttribute(p),
71 set: (value) => setAttribute(p, value)
72 });
73 });
74
75 Object.defineProperty(element, "firstChild", {
76 get: getFirstChild
77 });
78
79 Object.defineProperty(element, "firstElementChild", {
80 get: getFirstChildElement
81 });
82
83 Object.defineProperty(element, "lastChild", {
84 get: getLastChild
85 });
86
87 Object.defineProperty(element, "lastElementChild", {
88 get: getLastChildElement
89 });
90
91 Object.defineProperty(element, "previousElementSibling", {
92 get: () => _getElement($elm.prev())
93 });
94
95 Object.defineProperty(element, "nextElementSibling", {
96 get: () => _getElement($elm.next())
97 });
98
99 Object.defineProperty(element, "children", {
100 get: getChildren
101 });
102
103 Object.defineProperty(element, "parentElement", {
104 get: () => _getElement($elm.parent())
105 });
106
107 Object.defineProperty(element, "innerHTML", {
108 get: () => $elm.html(),
109 set: (value) => {
110 $elm.html(value);
111 if (tagName === "textarea") {
112 element.value = $elm.html();
113 }
114
115 emitter.emit("_insert");
116 }
117 });
118
119 Object.defineProperty(element, "innerText", {
120 get: () => element.textContent,
121 set: (value) => {
122 element.textContent = value;
123
124 if (tagName === "textarea") {
125 element.value = element.textContent;
126 }
127 }
128 });
129
130 Object.defineProperty(element, "textContent", {
131 get: () => {
132 return tagName === "script" ? $elm.html() : $elm.text();
133 },
134 set: (value) => {
135 const response = tagName === "script" ? $elm.html(value) : $elm.text(value);
136 emitter.emit("_insert");
137 return response;
138 }
139 });
140
141 Object.defineProperty(element, "outerHTML", {
142 get: () => {
143 return $.html($elm);
144 },
145 set: (value) => {
146 $elm.replaceWith($(value));
147 emitter.emit("_insert");
148 }
149 });
150
151 Object.defineProperty(element, "href", {
152 get: () => {
153 const rel = getAttribute("href");
154 return makeAbsolute(location, rel);
155 },
156 set: (value) => {
157 setAttribute("href", value);
158 }
159 });
160
161 Object.defineProperty(element, "src", {
162 get: () => {
163 const rel = getAttribute("src");
164 return makeAbsolute(location, rel);
165 },
166 set: (value) => {
167 setAttribute("src", value);
168 dispatchEvent(new Event("load", {bubbles: true}));
169 }
170 });
171
172 Object.defineProperty(element, "content", {
173 get: () => {
174 if (tagName !== "template") return;
175 return DocumentFragment(Element, element);
176 }
177 });
178
179 Object.defineProperty(element, "checked", {
180 get: () => getProperty("checked"),
181 set: (value) => {
182 if ($elm.attr("type") === "radio") radioButtonChecked(value);
183 else if ($elm.attr("type") === "checkbox") checkboxChecked(value);
184 }
185 });
186
187 Object.defineProperty(element, "options", {
188 get: () => getChildren("option")
189 });
190
191 Object.defineProperty(element, "selected", {
192 get: () => getProperty("selected"),
193 set: (value) => {
194 const oldValue = getProperty("selected");
195 const $select = $elm.parent("select");
196 if (!$select.attr("multiple")) {
197 if (value) $elm.siblings("option").prop("selected", false);
198 }
199
200 setProperty("selected", value);
201
202 if (value !== oldValue) {
203 _getElement($select).dispatchEvent(new Event("change", { bubbles: true }));
204 }
205
206 return value;
207 }
208 });
209
210 Object.defineProperty(element, "selectedIndex", {
211 get: () => getChildren("option").findIndex((option) => option.selected)
212 });
213
214 Object.defineProperty(element, "disabled", {
215 get: () => {
216 const value = getAttribute("disabled");
217 if (value === undefined) {
218 if (!inputElements.includes(tagName)) return;
219 }
220 return value === "disabled";
221 },
222 set: (value) => {
223 if (value === true) return setAttribute("disabled", "disabled");
224 $elm.removeAttr("disabled");
225 }
226 });
227
228 Object.defineProperty(element, "className", {
229 get: () => $elm.attr("class"),
230 set: (value) => $elm.attr("class", value)
231 });
232
233 Object.defineProperty(element, "form", {
234 get: () => _getElement($elm.closest("form"))
235 });
236
237 Object.defineProperty(element, "offsetWidth", {
238 get: () => getBoundingClientRect().width
239 });
240
241 Object.defineProperty(element, "offsetHeight", {
242 get: () => getBoundingClientRect().height
243 });
244
245 Object.defineProperty(element, "dataset", {
246 get: () => Dataset($elm)
247 });
248
249 Object.defineProperty(element, "nodeType", {
250 get: () => 1
251 });
252
253 Object.defineProperty(element, "scrollWidth", {
254 get: () => {
255 return element.children.reduce((acc, el) => {
256 acc += el.getBoundingClientRect().width;
257 return acc;
258 }, 0);
259 }
260 });
261
262 Object.defineProperty(element, "value", {
263 get: () => {
264 if (tagName === "select") {
265 const selectedIndex = element.selectedIndex;
266 if (selectedIndex < 0) return "";
267 return element.options[selectedIndex].innerText;
268 }
269
270 if (!inputElements.includes(tagName)) return;
271 const value = getAttribute("value");
272 if (value === undefined) return "";
273 return value;
274 },
275 set: (value) => {
276 if (!inputElements.includes(tagName)) return;
277 setAttribute("value", value);
278 }
279 });
280
281 Object.defineProperty(element, "elements", {
282 get() {
283 if (tagName !== "form") return;
284 return $elm.find("input,button,select,textarea").map(toElement).toArray();
285 }
286 });
287
288 let currentScrollLeft = 0;
289 Object.defineProperty(element, "scrollLeft", {
290 get: () => currentScrollLeft,
291 set: (value) => {
292 const scrollWidth = element.scrollWidth;
293 if (value > scrollWidth) value = scrollWidth;
294 else if (value < 0) value = 0;
295
296 onElementScroll(value);
297 currentScrollLeft = value;
298 dispatchEvent(new Event("scroll", { bubbles: true }));
299 }
300 });
301
302 let elementsToScroll = () => {};
303 function setElementsToScroll(elmsToScrollFn) {
304 elementsToScroll = elmsToScrollFn;
305 }
306
307 function onElementScroll(scrollLeft) {
308 if (!elementsToScroll) return;
309 const elms = elementsToScroll(document);
310 if (!elms || !elms.length) return;
311
312 const delta = currentScrollLeft - scrollLeft;
313
314 elms.slice().forEach((elm) => {
315 const {left, right} = elm.getBoundingClientRect();
316 elm._setBoundingClientRect({
317 left: (left || 0) + delta,
318 right: (right || 0) + delta
319 });
320 });
321 }
322
323 if (tagName === "form") {
324 element.submit = submit;
325 element.reset = reset;
326 }
327
328 if (tagName === "video") {
329 element.play = () => {
330 return Promise.resolve(undefined);
331 };
332
333 element.pause = () => {
334 return undefined;
335 };
336
337 element.load = () => {
338 };
339
340 element.canPlayType = function canPlayType() {
341 return "maybe";
342 };
343 }
344
345 Object.assign(element, EventListeners(element));
346
347 emitter.on("_insert", () => {
348 if (element.parentElement) {
349 element.parentElement._emitter.emit("_insert");
350 }
351 }).on("_attributeChange", (...args) => {
352 if (element.parentElement) {
353 element.parentElement._emitter.emit("_attributeChange", ...args);
354 }
355 });
356
357 return element;
358
359 function getFirstChildElement() {
360 const firstChild = find("> :first-child");
361 if (!firstChild.length) return null;
362 return _getElement(firstChild);
363 }
364
365 function getLastChildElement() {
366 const lastChild = find("> :last-child");
367 if (!lastChild.length) return null;
368 return _getElement(find("> :last-child"));
369 }
370
371 function getFirstChild() {
372 const firstChild = $elm[0].children[0];
373 if (!firstChild) return null;
374 if (firstChild.type === "text") return firstChild.data;
375 return getFirstChildElement();
376 }
377
378 function getLastChild() {
379 const elmChildren = $elm[0].children;
380 if (!elmChildren.length) return null;
381 const lastChild = elmChildren[elmChildren.length - 1];
382 if (lastChild.type === "text") return lastChild.data;
383 return getLastChildElement();
384 }
385
386 function getElementsByClassName(query) {
387 return find(`.${query}`).map(toElement).toArray();
388 }
389
390 function getElementsByTagName(query) {
391 return find(`${query}`).map((idx, elm) => _getElement($(elm))).toArray();
392 }
393
394 function appendChild(childElement) {
395 if (childElement instanceof DocumentFragment) {
396 insertAdjacentHTML("beforeend", childElement._getContent());
397 } else if (childElement.$elm) {
398 $elm.append(childElement.$elm);
399
400 if (childElement.$elm[0].tagName === "script") {
401 vm.runInNewContext(childElement.innerText, document.window);
402 }
403
404 emitter.emit("_insert");
405 } else if (childElement.textContent) {
406 insertAdjacentHTML("beforeend", childElement.textContent);
407 }
408 }
409
410 function click() {
411 if (element.disabled) return;
412 const clickEvent = new Event("click", { bubbles: true });
413
414 let changed = false;
415 if (element.type === "radio" || element.type === "checkbox") {
416 changed = !element.checked;
417 element.checked = true;
418 }
419
420 dispatchEvent(clickEvent);
421
422 if (!clickEvent.defaultPrevented && element.form) {
423 if (changed) {
424 dispatchEvent(new Event("change", { bubbles: true }));
425 } else if (!element.type || element.type === "submit") {
426 const submitEvent = new Event("submit", { bubbles: true });
427 submitEvent._submitElement = element;
428 element.form.dispatchEvent(submitEvent);
429 } else if (element.type === "reset") {
430 element.form.reset();
431 }
432 }
433 }
434
435 function contains(el) {
436 return $elm === el.$elm || $elm.find(el.$elm).length > 0;
437 }
438
439 function matches(selector) {
440 try {
441 return $elm.is(selector);
442 } catch (error) {
443 throw new DOMException(`Failed to execute 'matches' on 'Element': '${selector}' is not a valid selector.`, "SyntaxError");
444 }
445 }
446
447 function dispatchEvent(event) {
448 if (event.cancelBubble) return;
449 event.path.push(element);
450 if (!event.target) {
451 event.target = element;
452 }
453 emitter.emit(event.type, event);
454 if (event.bubbles) {
455 if (element.parentElement) return element.parentElement.dispatchEvent(event);
456
457 if (document && document.firstElementChild === element) {
458 document.dispatchEvent(event);
459 }
460 }
461 }
462
463 function getAttribute(name) {
464 return $elm.attr(name);
465 }
466
467 function hasAttribute(name) {
468 return $elm.is(`[${name}]`);
469 }
470
471 function getProperty(name) {
472 return $elm.prop(name);
473 }
474
475 function setProperty(name, val) {
476 return $elm.prop(name, val);
477 }
478
479 function removeChild(childElement) {
480 emitter.emit("_insert");
481 childElement.$elm.remove();
482 }
483
484 function remove() {
485 $elm.remove();
486 }
487
488 function find(selector) {
489 return $elm.find(selector);
490 }
491
492 function closest(selector) {
493 return _getElement($elm.closest(selector));
494 }
495
496 function getChildren(selector) {
497 if (!$elm) return [];
498 return $elm.children(selector).map(toElement).toArray();
499 }
500
501 function insertAdjacentHTML(position, markup) {
502 switch (position) {
503 case "beforebegin":
504 $elm.before(markup);
505 if (element.parentElement) element.parentElement._emitter.emit("_insert");
506 break;
507 case "afterbegin":
508 $elm.prepend(markup);
509 emitter.emit("_insert");
510 break;
511 case "beforeend":
512 $elm.append(markup);
513 emitter.emit("_insert");
514 break;
515 case "afterend":
516 $elm.after(markup);
517 if (element.parentElement) element.parentElement._emitter.emit("_insert");
518 break;
519 default:
520 throw new DOMException(`Failed to execute 'insertAdjacentHTML' on 'Element': The value provided (${position}) is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'.`);
521 }
522 }
523
524 function insertBefore(newNode, referenceNode) {
525 if (referenceNode === null) {
526 element.appendChild(newNode);
527 return newNode;
528 }
529
530 if (newNode.$elm) {
531 if (referenceNode.parentElement !== element) {
532 throw new DOMException("Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.");
533 }
534 newNode.$elm.insertBefore(referenceNode.$elm);
535 emitter.emit("_insert");
536 return newNode;
537 }
538
539 if (newNode.textContent) {
540 referenceNode.$elm.before(newNode.textContent);
541 emitter.emit("_insert");
542 return newNode;
543 }
544 }
545
546 function setBoundingClientRect(axes) {
547 if (!("bottom" in axes)) {
548 axes.bottom = axes.top;
549 }
550
551 for (const axis in axes) {
552 if (axes.hasOwnProperty(axis)) {
553 rects[axis] = axes[axis];
554 }
555 }
556
557 rects.height = rects.bottom - rects.top;
558 rects.width = rects.right - rects.left;
559
560 return rects;
561 }
562
563 function getBoundingClientRect() {
564 return rects;
565 }
566
567 function setAttribute(name, val) {
568 $elm.attr(name, val);
569 emitter.emit("_attributeChange", name, element);
570 }
571
572 function removeAttribute(name) {
573 $elm.removeAttr(name);
574 }
575
576 function requestFullscreen() {
577 const fullscreenchangeEvent = new Event("fullscreenchange", { bubbles: true });
578 fullscreenchangeEvent.target = element;
579
580 document.dispatchEvent(fullscreenchangeEvent);
581 }
582
583 function cloneNode(deep) {
584 const $clone = $elm.clone();
585 if (!deep) {
586 $clone.empty();
587 }
588 return _getElement($clone);
589 }
590
591 function radioButtonChecked(value) {
592 uncheckRadioButtons();
593 setAttribute("checked", value);
594 }
595
596 function checkboxChecked(value) {
597 setProperty("checked", value);
598 }
599
600 function uncheckRadioButtons() {
601 if ($elm.attr("type") !== "radio") return;
602
603 const name = $elm.attr("name");
604
605 const $form = $elm.closest("form");
606 if ($form && $form.length) {
607 return $form.find(`input[type="radio"][name="${name}"]`).removeAttr("checked");
608 }
609
610 $(`input[type="radio"][name="${name}"]`).removeAttr("checked");
611 }
612
613 function toElement(idx, elm) {
614 return _getElement($(elm));
615 }
616
617 function submit() {
618 dispatchEvent(new Event("submit", { bubbles: true }));
619 }
620
621 function reset() {
622 const $inputs = find("input[type='checkbox']");
623 if ($inputs.length) {
624 $inputs.each((idx, elm) => {
625 $(elm).prop("checked", !!$(elm).attr("checked"));
626 });
627 }
628
629 const $options = find("option");
630 if ($options.length) {
631 $options.each((idx, elm) => {
632 $(elm).prop("selected", !!$(elm).attr("selected"));
633 });
634 }
635
636 dispatchEvent(new Event("reset", { bubbles: true }));
637 }
638
639 function getClassList() {
640 if (!$elm.attr) return;
641
642 const classListApi = {
643 contains(className) {
644 return $elm.hasClass(className);
645 },
646 add(...classNames) {
647 $elm.addClass(classNames.join(" "));
648 emitter.emit("_classadded", ...classNames);
649 emitter.emit("_attributeChange", "class", element);
650 },
651 remove(...classNames) {
652 $elm.removeClass(classNames.join(" "));
653 emitter.emit("_classremoved", ...classNames);
654 emitter.emit("_attributeChange", "class", element);
655 },
656 toggle(className, force) {
657 const hasClass = $elm.hasClass(className);
658
659 if (force === undefined) {
660 const methodName = this.contains(className) ? "remove" : "add";
661 this[methodName](className);
662 return !hasClass;
663 }
664
665 if (force) {
666 this.add(className);
667 } else {
668 this.remove(className);
669 }
670 return !hasClass;
671 }
672 };
673
674 Object.defineProperty(classListApi, "_classes", {
675 get: getClassArray
676 });
677
678 return classListApi;
679
680 function getClassArray() {
681 return ($elm.attr("class") || "").split(" ");
682 }
683 }
684
685 function getStyle() {
686 const elementStyle = getAttribute("style") || "";
687 const prefixNamePattern = /^(-?)(moz|ms|webkit)([A-Z]|\1)/;
688
689 const Style = {};
690 if (elementStyle) {
691 elementStyle.replace(/\s*(.+?):\s*(.*?)(;|$)/g, (_, name, value) => {
692 let ccName = name.replace(prefixNamePattern, (__, isPrefix, prefix, suffix) => {
693 return prefix + suffix;
694 });
695 ccName = ccName.replace(/-(\w)(\w+)/g, (__, firstLetter, rest) => `${firstLetter.toUpperCase()}${rest}`);
696 Style[ccName] = value;
697 });
698 }
699
700 Object.defineProperty(Style, "removeProperty", {
701 enumerable: false,
702 value: removeProperty
703 });
704
705 const StyleHandler = {
706 set: (target, name, value) => {
707 if (!name) return false;
708 target[name] = value;
709 setStyle();
710 return true;
711 },
712 deleteProperty: (target, name) => {
713 if (!name) return false;
714 delete target[name];
715 setStyle();
716 return true;
717 }
718 };
719
720 return new Proxy(Style, StyleHandler);
721
722 function removeProperty(name) {
723 delete Style[name];
724 setStyle();
725 }
726
727 function setStyle() {
728 const keys = Object.keys(Style);
729 if (!keys.length) return removeAttribute("style");
730 const styleValue = keys.reduce((result, name) => {
731 const value = Style[name];
732 if (value === undefined || value === "") return result;
733
734 let kcName = name.replace(prefixNamePattern, (__, isPrefix, prefix, suffix) => {
735 return `-${prefix}${suffix}`;
736 });
737
738 kcName = kcName.replace(/([A-Z])([a-z]+)/g, (__, firstLetter, rest) => `-${firstLetter.toLowerCase()}${rest}`);
739
740 result += `${kcName}: ${value};`;
741 return result;
742 }, "");
743
744 if (!styleValue) return removeAttribute("style");
745 setAttribute("style", styleValue);
746 }
747 }
748}
749
750function Dataset($elm) {
751 if (!$elm || !$elm[0]) return;
752 return makeProxy(get());
753
754 function get() {
755 if (!$elm[0].attribs) return {};
756 const {attribs} = $elm[0];
757 return Object.keys(attribs).reduce((acc, key) => {
758 if (key.startsWith("data-")) {
759 acc[key.replace(/^data-/, "").replace(/-(\w)/g, (a, b) => b.toUpperCase())] = attribs[key];
760 }
761 return acc;
762 }, {});
763 }
764
765 function makeProxy(attributes = {}) {
766 return new Proxy(attributes, {
767 set: setDataset
768 });
769 }
770
771 function setDataset(_, prop, value) {
772 if (!$elm || !$elm[0] || !$elm[0].attribs) return false;
773 const key = prop.replace(/[A-Z]/g, (g) => `-${g[0].toLowerCase()}`);
774 $elm.attr(`data-${key}`, value);
775 return true;
776 }
777}
778
779function EventListeners(element) {
780 const listeners = [];
781 return {
782 addEventListener,
783 removeEventListener
784 };
785
786 function addEventListener(name, fn, options) {
787 const config = [name, fn, usesCapture(options), boundFn];
788 const existingListenerIndex = getExistingIndex(...config);
789 if (existingListenerIndex !== -1) return;
790
791 listeners.push(config);
792 element._emitter.on(name, boundFn);
793
794 function boundFn(...args) {
795 fn.apply(element, args);
796
797 if (options && options.once) {
798 removeEventListener(name, fn);
799 }
800 }
801 }
802
803 function removeEventListener(name, fn, options) {
804 const existingListenerIndex = getExistingIndex(name, fn, usesCapture(options));
805 if (existingListenerIndex === -1) return;
806
807 const existingListener = listeners[existingListenerIndex];
808 const boundFn = existingListener[3];
809
810 element._emitter.removeListener(name, boundFn);
811 listeners.splice(existingListenerIndex, 1);
812 }
813
814 function usesCapture(options) {
815 if (typeof options === "object") {
816 return !!options.capture;
817 }
818
819 return !!options;
820 }
821
822 function getExistingIndex(...config) {
823 return listeners.findIndex((listener) => {
824 return listener[0] === config[0]
825 && listener[1] === config[1]
826 && listener[2] === config[2];
827 });
828 }
829}