UNPKG

21.6 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, "selectedOptions", {
215 get() {
216 return element.options ? element.options.filter((option) => option.selected) : undefined;
217 }
218 });
219
220 Object.defineProperty(element, "disabled", {
221 get: () => {
222 const value = getAttribute("disabled");
223 if (value === undefined) {
224 if (!inputElements.includes(tagName)) return;
225 }
226 return value === "disabled";
227 },
228 set: (value) => {
229 if (value === true) return setAttribute("disabled", "disabled");
230 $elm.removeAttr("disabled");
231 }
232 });
233
234 Object.defineProperty(element, "className", {
235 get: () => $elm.attr("class"),
236 set: (value) => $elm.attr("class", value)
237 });
238
239 Object.defineProperty(element, "form", {
240 get: () => _getElement($elm.closest("form"))
241 });
242
243 Object.defineProperty(element, "offsetWidth", {
244 get: () => getBoundingClientRect().width
245 });
246
247 Object.defineProperty(element, "offsetHeight", {
248 get: () => getBoundingClientRect().height
249 });
250
251 Object.defineProperty(element, "dataset", {
252 get: () => Dataset($elm)
253 });
254
255 Object.defineProperty(element, "nodeType", {
256 get: () => 1
257 });
258
259 Object.defineProperty(element, "scrollWidth", {
260 get: () => {
261 return element.children.reduce((acc, el) => {
262 acc += el.getBoundingClientRect().width;
263 return acc;
264 }, 0);
265 }
266 });
267
268 Object.defineProperty(element, "value", {
269 get: () => {
270 if (element.tagName === "SELECT") {
271 const selectedIndex = element.selectedIndex;
272 if (selectedIndex < 0) return "";
273 const option = element.options[selectedIndex];
274 if (option.hasAttribute("value")) {
275 return option.getAttribute("value");
276 }
277 return option.innerText;
278 } else if (element.tagName === "OPTION") {
279 if (element.hasAttribute("value")) {
280 return element.getAttribute("value");
281 }
282 return "";
283 }
284
285 if (!inputElements.includes(tagName)) return;
286 const value = getAttribute("value");
287 if (value === undefined) return "";
288 return value;
289 },
290 set: (value) => {
291 if (!inputElements.includes(tagName)) return;
292 setAttribute("value", value);
293 }
294 });
295
296 Object.defineProperty(element, "elements", {
297 get() {
298 if (tagName !== "form") return;
299 return $elm.find("input,button,select,textarea").map(toElement).toArray();
300 }
301 });
302
303 let currentScrollLeft = 0;
304 Object.defineProperty(element, "scrollLeft", {
305 get: () => currentScrollLeft,
306 set: (value) => {
307 const scrollWidth = element.scrollWidth;
308 if (value > scrollWidth) value = scrollWidth;
309 else if (value < 0) value = 0;
310
311 onElementScroll(value);
312 currentScrollLeft = value;
313 dispatchEvent(new Event("scroll", { bubbles: true }));
314 }
315 });
316
317 let elementsToScroll = () => {};
318 function setElementsToScroll(elmsToScrollFn) {
319 elementsToScroll = elmsToScrollFn;
320 }
321
322 function onElementScroll(scrollLeft) {
323 if (!elementsToScroll) return;
324 const elms = elementsToScroll(document);
325 if (!elms || !elms.length) return;
326
327 const delta = currentScrollLeft - scrollLeft;
328
329 elms.slice().forEach((elm) => {
330 const {left, right} = elm.getBoundingClientRect();
331 elm._setBoundingClientRect({
332 left: (left || 0) + delta,
333 right: (right || 0) + delta
334 });
335 });
336 }
337
338 if (tagName === "form") {
339 element.submit = submit;
340 element.reset = reset;
341 }
342
343 if (tagName === "video") {
344 element.play = () => {
345 return Promise.resolve(undefined);
346 };
347
348 element.pause = () => {
349 return undefined;
350 };
351
352 element.load = () => {
353 };
354
355 element.canPlayType = function canPlayType() {
356 return "maybe";
357 };
358 }
359
360 Object.assign(element, EventListeners(element));
361
362 emitter.on("_insert", () => {
363 if (element.parentElement) {
364 element.parentElement._emitter.emit("_insert");
365 }
366 }).on("_attributeChange", (...args) => {
367 if (element.parentElement) {
368 element.parentElement._emitter.emit("_attributeChange", ...args);
369 }
370 });
371
372 return element;
373
374 function getFirstChildElement() {
375 const firstChild = find("> :first-child");
376 if (!firstChild.length) return null;
377 return _getElement(firstChild);
378 }
379
380 function getLastChildElement() {
381 const lastChild = find("> :last-child");
382 if (!lastChild.length) return null;
383 return _getElement(find("> :last-child"));
384 }
385
386 function getFirstChild() {
387 const firstChild = $elm[0].children[0];
388 if (!firstChild) return null;
389 if (firstChild.type === "text") return firstChild.data;
390 return getFirstChildElement();
391 }
392
393 function getLastChild() {
394 const elmChildren = $elm[0].children;
395 if (!elmChildren.length) return null;
396 const lastChild = elmChildren[elmChildren.length - 1];
397 if (lastChild.type === "text") return lastChild.data;
398 return getLastChildElement();
399 }
400
401 function getElementsByClassName(query) {
402 return find(`.${query}`).map(toElement).toArray();
403 }
404
405 function getElementsByTagName(query) {
406 return find(`${query}`).map((idx, elm) => _getElement($(elm))).toArray();
407 }
408
409 function appendChild(childElement) {
410 if (childElement instanceof DocumentFragment) {
411 insertAdjacentHTML("beforeend", childElement._getContent());
412 } else if (childElement.$elm) {
413 $elm.append(childElement.$elm);
414
415 if (childElement.$elm[0].tagName === "script") {
416 vm.runInNewContext(childElement.innerText, document.window);
417 }
418
419 emitter.emit("_insert");
420 } else if (childElement.textContent) {
421 insertAdjacentHTML("beforeend", childElement.textContent);
422 }
423 }
424
425 function click() {
426 if (element.disabled) return;
427 const clickEvent = new Event("click", { bubbles: true });
428
429 let changed = false;
430 if (element.type === "radio" || element.type === "checkbox") {
431 changed = !element.checked;
432 element.checked = true;
433 }
434
435 dispatchEvent(clickEvent);
436
437 if (!clickEvent.defaultPrevented && element.form) {
438 if (changed) {
439 dispatchEvent(new Event("change", { bubbles: true }));
440 } else if (!element.type || element.type === "submit") {
441 const submitEvent = new Event("submit", { bubbles: true });
442 submitEvent._submitElement = element;
443 element.form.dispatchEvent(submitEvent);
444 } else if (element.type === "reset") {
445 element.form.reset();
446 }
447 }
448 }
449
450 function contains(el) {
451 return $elm === el.$elm || $elm.find(el.$elm).length > 0;
452 }
453
454 function matches(selector) {
455 try {
456 return $elm.is(selector);
457 } catch (error) {
458 throw new DOMException(`Failed to execute 'matches' on 'Element': '${selector}' is not a valid selector.`, "SyntaxError");
459 }
460 }
461
462 function dispatchEvent(event) {
463 if (event.cancelBubble) return;
464 event.path.push(element);
465 if (!event.target) {
466 event.target = element;
467 }
468 emitter.emit(event.type, event);
469 if (event.bubbles) {
470 if (element.parentElement) return element.parentElement.dispatchEvent(event);
471
472 if (document && document.firstElementChild === element) {
473 document.dispatchEvent(event);
474 }
475 }
476 }
477
478 function getAttribute(name) {
479 return $elm.attr(name);
480 }
481
482 function hasAttribute(name) {
483 return $elm.is(`[${name}]`);
484 }
485
486 function getProperty(name) {
487 return $elm.prop(name);
488 }
489
490 function setProperty(name, val) {
491 return $elm.prop(name, val);
492 }
493
494 function removeChild(childElement) {
495 emitter.emit("_insert");
496 childElement.$elm.remove();
497 }
498
499 function remove() {
500 $elm.remove();
501 }
502
503 function find(selector) {
504 return $elm.find(selector);
505 }
506
507 function closest(selector) {
508 return _getElement($elm.closest(selector));
509 }
510
511 function getChildren(selector) {
512 if (!$elm) return [];
513 return $elm.children(selector).map(toElement).toArray();
514 }
515
516 function insertAdjacentHTML(position, markup) {
517 switch (position) {
518 case "beforebegin":
519 $elm.before(markup);
520 if (element.parentElement) element.parentElement._emitter.emit("_insert");
521 break;
522 case "afterbegin":
523 $elm.prepend(markup);
524 emitter.emit("_insert");
525 break;
526 case "beforeend":
527 $elm.append(markup);
528 emitter.emit("_insert");
529 break;
530 case "afterend":
531 $elm.after(markup);
532 if (element.parentElement) element.parentElement._emitter.emit("_insert");
533 break;
534 default:
535 throw new DOMException(`Failed to execute 'insertAdjacentHTML' on 'Element': The value provided (${position}) is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'.`);
536 }
537 }
538
539 function insertBefore(newNode, referenceNode) {
540 if (referenceNode === null) {
541 element.appendChild(newNode);
542 return newNode;
543 }
544
545 if (newNode.$elm) {
546 if (referenceNode.parentElement !== element) {
547 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.");
548 }
549 newNode.$elm.insertBefore(referenceNode.$elm);
550 emitter.emit("_insert");
551 return newNode;
552 }
553
554 if (newNode.textContent) {
555 referenceNode.$elm.before(newNode.textContent);
556 emitter.emit("_insert");
557 return newNode;
558 }
559 }
560
561 function setBoundingClientRect(axes) {
562 if (!("bottom" in axes)) {
563 axes.bottom = axes.top;
564 }
565
566 for (const axis in axes) {
567 if (axes.hasOwnProperty(axis)) {
568 rects[axis] = axes[axis];
569 }
570 }
571
572 rects.height = rects.bottom - rects.top;
573 rects.width = rects.right - rects.left;
574
575 return rects;
576 }
577
578 function getBoundingClientRect() {
579 return rects;
580 }
581
582 function setAttribute(name, val) {
583 $elm.attr(name, val);
584 emitter.emit("_attributeChange", name, element);
585 }
586
587 function removeAttribute(name) {
588 $elm.removeAttr(name);
589 }
590
591 function requestFullscreen() {
592 const fullscreenchangeEvent = new Event("fullscreenchange", { bubbles: true });
593 fullscreenchangeEvent.target = element;
594
595 document.dispatchEvent(fullscreenchangeEvent);
596 }
597
598 function cloneNode(deep) {
599 const $clone = $elm.clone();
600 if (!deep) {
601 $clone.empty();
602 }
603 return _getElement($clone);
604 }
605
606 function radioButtonChecked(value) {
607 uncheckRadioButtons();
608 setAttribute("checked", value);
609 }
610
611 function checkboxChecked(value) {
612 setProperty("checked", value);
613 }
614
615 function uncheckRadioButtons() {
616 if ($elm.attr("type") !== "radio") return;
617
618 const name = $elm.attr("name");
619
620 const $form = $elm.closest("form");
621 if ($form && $form.length) {
622 return $form.find(`input[type="radio"][name="${name}"]`).removeAttr("checked");
623 }
624
625 $(`input[type="radio"][name="${name}"]`).removeAttr("checked");
626 }
627
628 function toElement(idx, elm) {
629 return _getElement($(elm));
630 }
631
632 function submit() {
633 dispatchEvent(new Event("submit", { bubbles: true }));
634 }
635
636 function reset() {
637 const $inputs = find("input[type='checkbox']");
638 if ($inputs.length) {
639 $inputs.each((idx, elm) => {
640 $(elm).prop("checked", !!$(elm).attr("checked"));
641 });
642 }
643
644 const $options = find("option");
645 if ($options.length) {
646 $options.each((idx, elm) => {
647 $(elm).prop("selected", !!$(elm).attr("selected"));
648 });
649 }
650
651 dispatchEvent(new Event("reset", { bubbles: true }));
652 }
653
654 function getClassList() {
655 if (!$elm.attr) return;
656
657 const classListApi = {
658 contains(className) {
659 return $elm.hasClass(className);
660 },
661 add(...classNames) {
662 $elm.addClass(classNames.join(" "));
663 emitter.emit("_classadded", ...classNames);
664 emitter.emit("_attributeChange", "class", element);
665 },
666 remove(...classNames) {
667 $elm.removeClass(classNames.join(" "));
668 emitter.emit("_classremoved", ...classNames);
669 emitter.emit("_attributeChange", "class", element);
670 },
671 toggle(className, force) {
672 const hasClass = $elm.hasClass(className);
673
674 if (force === undefined) {
675 const methodName = this.contains(className) ? "remove" : "add";
676 this[methodName](className);
677 return !hasClass;
678 }
679
680 if (force) {
681 this.add(className);
682 } else {
683 this.remove(className);
684 }
685 return !hasClass;
686 }
687 };
688
689 Object.defineProperty(classListApi, "_classes", {
690 get: getClassArray
691 });
692
693 return classListApi;
694
695 function getClassArray() {
696 return ($elm.attr("class") || "").split(" ");
697 }
698 }
699
700 function getStyle() {
701 const elementStyle = getAttribute("style") || "";
702 const prefixNamePattern = /^(-?)(moz|ms|webkit)([A-Z]|\1)/;
703
704 const Style = {};
705 if (elementStyle) {
706 elementStyle.replace(/\s*(.+?):\s*(.*?)(;|$)/g, (_, name, value) => {
707 let ccName = name.replace(prefixNamePattern, (__, isPrefix, prefix, suffix) => {
708 return prefix + suffix;
709 });
710 ccName = ccName.replace(/-(\w)(\w+)/g, (__, firstLetter, rest) => `${firstLetter.toUpperCase()}${rest}`);
711 Style[ccName] = value;
712 });
713 }
714
715 Object.defineProperty(Style, "removeProperty", {
716 enumerable: false,
717 value: removeProperty
718 });
719
720 const StyleHandler = {
721 set: (target, name, value) => {
722 if (!name) return false;
723 target[name] = value;
724 setStyle();
725 return true;
726 },
727 deleteProperty: (target, name) => {
728 if (!name) return false;
729 delete target[name];
730 setStyle();
731 return true;
732 }
733 };
734
735 return new Proxy(Style, StyleHandler);
736
737 function removeProperty(name) {
738 delete Style[name];
739 setStyle();
740 }
741
742 function setStyle() {
743 const keys = Object.keys(Style);
744 if (!keys.length) return removeAttribute("style");
745 const styleValue = keys.reduce((result, name) => {
746 const value = Style[name];
747 if (value === undefined || value === "") return result;
748
749 let kcName = name.replace(prefixNamePattern, (__, isPrefix, prefix, suffix) => {
750 return `-${prefix}${suffix}`;
751 });
752
753 kcName = kcName.replace(/([A-Z])([a-z]+)/g, (__, firstLetter, rest) => `-${firstLetter.toLowerCase()}${rest}`);
754
755 result += `${kcName}: ${value};`;
756 return result;
757 }, "");
758
759 if (!styleValue) return removeAttribute("style");
760 setAttribute("style", styleValue);
761 }
762 }
763}
764
765function Dataset($elm) {
766 if (!$elm || !$elm[0]) return;
767 return makeProxy(get());
768
769 function get() {
770 if (!$elm[0].attribs) return {};
771 const {attribs} = $elm[0];
772 return Object.keys(attribs).reduce((acc, key) => {
773 if (key.startsWith("data-")) {
774 acc[key.replace(/^data-/, "").replace(/-(\w)/g, (a, b) => b.toUpperCase())] = attribs[key];
775 }
776 return acc;
777 }, {});
778 }
779
780 function makeProxy(attributes = {}) {
781 return new Proxy(attributes, {
782 set: setDataset
783 });
784 }
785
786 function setDataset(_, prop, value) {
787 if (!$elm || !$elm[0] || !$elm[0].attribs) return false;
788 const key = prop.replace(/[A-Z]/g, (g) => `-${g[0].toLowerCase()}`);
789 $elm.attr(`data-${key}`, value);
790 return true;
791 }
792}
793
794function EventListeners(element) {
795 const listeners = [];
796 return {
797 addEventListener,
798 removeEventListener
799 };
800
801 function addEventListener(name, fn, options) {
802 const config = [name, fn, usesCapture(options), boundFn];
803 const existingListenerIndex = getExistingIndex(...config);
804 if (existingListenerIndex !== -1) return;
805
806 listeners.push(config);
807 element._emitter.on(name, boundFn);
808
809 function boundFn(...args) {
810 fn.apply(element, args);
811
812 if (options && options.once) {
813 removeEventListener(name, fn);
814 }
815 }
816 }
817
818 function removeEventListener(name, fn, options) {
819 const existingListenerIndex = getExistingIndex(name, fn, usesCapture(options));
820 if (existingListenerIndex === -1) return;
821
822 const existingListener = listeners[existingListenerIndex];
823 const boundFn = existingListener[3];
824
825 element._emitter.removeListener(name, boundFn);
826 listeners.splice(existingListenerIndex, 1);
827 }
828
829 function usesCapture(options) {
830 if (typeof options === "object") {
831 return !!options.capture;
832 }
833
834 return !!options;
835 }
836
837 function getExistingIndex(...config) {
838 return listeners.findIndex((listener) => {
839 return listener[0] === config[0]
840 && listener[1] === config[1]
841 && listener[2] === config[2];
842 });
843 }
844}