templates/js/library/Dom.js line 1

Open in your IDE?
  1. class ElementViewPortVisibilityChecker {
  2.     viewPortCheckElementsData = [];
  3.     removeViewPortElementsOnScrollCheck = null;
  4.     viewPortInterval = null;
  5.     rootElement = null;
  6.     callback = null;
  7.     options = null;
  8.     isBusy = false;
  9.     constructor(rootElement = null, callback = null, options = null) {
  10.         this.options = {
  11.             calcElementOptions: {
  12.                 offsetParent: true,
  13.                 calcScrollTop: false,
  14.                 ...(options?.calcElementOptions || {})
  15.             },
  16.             ...(options || {})
  17.         }
  18.        this.rootElement = rootElement;
  19.        this.callback = callback;
  20.     }
  21.     start() {
  22.         this.startViewPortElementsOnScrollCheck();
  23.         this.isBusy = false;
  24.         this.viewPortInterval = setInterval(async () => {
  25.             try {
  26.                 await this.viewPortElementsCheck();
  27.             } catch (e) {
  28.                 throw e;
  29.             }
  30.         }, 1000);
  31.     }
  32.     stop() {
  33.         if (this.removeViewPortElementsOnScrollCheck) {
  34.             this.removeViewPortElementsOnScrollCheck();
  35.             this.removeViewPortElementsOnScrollCheck = null;
  36.         }
  37.         if (this.viewPortInterval) {
  38.             clearInterval(this.viewPortInterval);
  39.             this.viewPortInterval = null;
  40.         }
  41.     }
  42.     clear() {
  43.         this.viewPortCheckElementsData = [];
  44.     }
  45.     startViewPortElementsOnScrollCheck() {
  46.         const onScroll = async () => {
  47.             await this.viewPortElementsCheck();
  48.         };
  49.         this.rootElement.addEventListener('scroll', onScroll);
  50.         this.removeViewPortElementsOnScrollCheck = () => {
  51.             this.rootElement.removeEventListener('scroll', onScroll);
  52.         }
  53.     }
  54.     addElement(data) {
  55.         // data = {el: null, callback: null, triggerOnce: false, ...data};
  56.         if (!this.findByElement(data.el)) {
  57.             this.viewPortCheckElementsData.push(data);
  58.         }
  59.     }
  60.     findByElement(el) {
  61.         return this.viewPortCheckElementsData.find(data => data.el === el);
  62.     }
  63.     async viewPortElementsCheck() {
  64.         if (this.isBusy) {
  65.             return;
  66.         }
  67.         this.isBusy = true;
  68.         try {
  69.             return await this._doViewPortElementsCheck();
  70.         } catch (e) {
  71.             throw e;
  72.         } finally {
  73.             this.isBusy = false;
  74.         }
  75.     }
  76.     async _doViewPortElementsCheck() {
  77.         let onCallbackTrue = (elementData) => {
  78.             if (elementData.triggerOnce) {
  79.                 for (let i = 0; i < this.viewPortCheckElementsData.length; i++) { //delete
  80.                     if (this.viewPortCheckElementsData[i] === elementData) {
  81.                         this.viewPortCheckElementsData.splice(i, 1);
  82.                         break;
  83.                     }
  84.                 }
  85.             }
  86.         };
  87.         let dataItems = [];
  88.         for (let data of this.viewPortCheckElementsData) {
  89.             let visibleData = Dom.getElementViewportData(data.el,
  90.                 this.rootElement, {
  91.                     calcElementOptions: this.options.calcElementOptions
  92.                 });
  93.             if ((!data.triggerOnce || !data.isTriggered) && data.el && visibleData.isVisible) {
  94.                 let dataItem = {elementData: data, visibleData};
  95.                 if (this.callback) {
  96.                     dataItems.push(dataItem);
  97.                 } else {
  98.                     if (data.callback(dataItem)) {
  99.                         onCallbackTrue(data);
  100.                     }
  101.                 }
  102.             }
  103.         }
  104.         if (this.callback && dataItems.length) {
  105.             let callbackResult = await this.callback({dataItems: dataItems, onDataItemHandled: (dataItem, handled) => {
  106.                 if (!dataItem || !dataItem.elementData) {
  107.                     return;
  108.                 }
  109.                 if (handled) {
  110.                     onCallbackTrue(dataItem.elementData);
  111.                 }
  112.             }});
  113.             if (callbackResult) {
  114.                 for (let dataItem of dataItems) {
  115.                     onCallbackTrue(dataItem.elementData);
  116.                 }
  117.             }
  118.         }
  119.     }
  120. }
  121. class Dom {
  122.     static viewPortCheckElementsData = [];
  123.     static mousePosition = { x: 0, y: 0 };
  124.     static onChange(target, callback, byChange = true, byKeyup = false, byProxy = false) {
  125.         function _callback(target, event = null) {
  126.             callback(target, event);
  127.         }
  128.         if (byChange) {
  129.             $(target).change(function (event) {
  130.                 _callback(target, event);
  131.             });
  132.         }
  133.         if (byKeyup) {
  134.             $("[data-route-param-name]").on("keyup", function (event) {
  135.                 _callback(target, event);
  136.             });
  137.         }
  138.         if (byProxy) {
  139.             const proxy = new Proxy(target, {
  140.                 set(target, property, value) {
  141.                     target[property] = value;
  142.                     if (property === 'value') {
  143.                         _callback(target, null);
  144.                     }
  145.                     return true;
  146.                 }
  147.             });
  148.             target.proxy = proxy;
  149.         }
  150.     }
  151.     static getElementAttrs(element, attrsNames) {
  152.         let result = {};
  153.         for (let attrName of attrsNames) {
  154.             result[attrName] = $(element).attr(attrName);
  155.         }
  156.         return result;
  157.     }
  158.     static isScrolledToBottom(element) {
  159.         return element.scrollHeight - element.scrollTop === element.clientHeight;
  160.     }
  161.     static getProp(name, elementOrSelector = null) {
  162.         let element = (elementOrSelector
  163.             ? (typeof elementOrSelector == 'string' ? $(elementOrSelector)[0] : elementOrSelector)
  164.             : document.documentElement);
  165.         return getComputedStyle(element).getPropertyValue(name) || document.documentElement.style.getPropertyValue(name);
  166.     }
  167.     static setProp(name, val, elementOrSelector = null) {
  168.         let element = (elementOrSelector
  169.             ? (typeof elementOrSelector == 'string' ? $(elementOrSelector)[0] : elementOrSelector)
  170.             : document.documentElement);
  171.         element.style.setProperty(name, val);
  172.     }
  173.     static getMediaSize() {
  174.         let result = Dom.getProp('--media-size');
  175.         if (result) {
  176.             result = result.replaceAll(/^["]|["]$/g, "");
  177.         }
  178.         return result;
  179.     }
  180.     static findParent(element, selector = null, topParent = null) {
  181.         if (selector !== null && !Array.isArray(selector)) {
  182.             selector = [selector];
  183.         }
  184.         if (topParent !== null && !Array.isArray(topParent)) {
  185.             topParent = [topParent];
  186.         }
  187.         while (element.parentElement) {
  188.             let withSelector = (selector !== null ? selector.find(current => $(element.parentElement).is(current)) : null);
  189.             if ((selector === null || withSelector) && (!topParent || topParent.find(current => element.parentElement == current))) {
  190.                 return element.parentElement;
  191.             }
  192.             element = element.parentElement;
  193.         }
  194.     }
  195.     static findParentChild(element, selector = null) {
  196.         if (selector !== null && !Array.isArray(selector)) {
  197.             selector = [selector];
  198.         }
  199.         while (element.parentElement) {
  200.             let child = null;
  201.             selector && selector.find(current => child = $(element.parentElement).find(current)[0]);
  202.             if (child) {
  203.                 return child;
  204.             }
  205.             element = element.parentElement;
  206.         }
  207.     }
  208.     static getElementViewportData(el, container = null, options = null) {
  209.         options = {
  210.             calcElementOptions: {
  211.                 offsetParent: true,
  212.                 calcScrollTop: true,
  213.                 ...(options?.calcElementOptions || {})
  214.             },
  215.             ...(options || {})
  216.         }
  217.         const rect = el.getBoundingClientRect();
  218.         let viewportHeight = null;
  219.         if (container) {
  220.             const containerRect = container.getBoundingClientRect();
  221.             viewportHeight = containerRect.height;
  222.         } else {
  223.             viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  224.             container = window.innerHeight ? window : document.documentElement;
  225.         }
  226.         const visibleTop = Dom.calcElementTop(el, null, options.calcElementOptions);
  227.         const visibleBottom = visibleTop + rect.height;
  228.         const containerTop = container === window ? 0 : Dom.calcElementTop(container, null, options.calcElementOptions);
  229.         const containerBottom = containerTop + viewportHeight;
  230.         const visiblePixels = Math.max(0, Math.min(visibleBottom, containerBottom) - Math.max(visibleTop, containerTop));
  231.         const isFullyVisible = visiblePixels >= rect.height;
  232.         const visibilityPercentage = visiblePixels / rect.height;
  233.         const isVisible = visiblePixels > 0;
  234.         return {
  235.             visiblePixels: Math.round(visiblePixels),
  236.             portHeight: viewportHeight,
  237.             visibilityPercentage,
  238.             isFullyVisible,
  239.             isVisible,
  240.         };
  241.     }
  242.     static findClosestElement(targetY, selector = null, elements = null, elementYOffset = null) {
  243.         if (selector) {
  244.             elements = document.querySelectorAll(selector);
  245.         }
  246.         // Initialize variables to keep track of the closest element and its distance
  247.         let closestElement = null;
  248.         let closestDistance = Infinity;
  249.         // Iterate over each element
  250.         elements.forEach(element => {
  251.             // Get the bounding rectangle of the element
  252.             const rect = element.getBoundingClientRect();
  253.             // Calculate the vertical distance from the targetY to the center of the element
  254.             const elementCenterY = element.offsetTop + rect.height / 2 + (elementYOffset ? elementYOffset : 0);
  255.             const distance = Math.abs(targetY - elementCenterY);
  256.             // If this element is closer than the current closest, update the closest element
  257.             if (distance < closestDistance) {
  258.                 closestDistance = distance;
  259.                 closestElement = element;
  260.             }
  261.         });
  262.         return closestElement;
  263.     }
  264.     static scrollElementIntoView(targetElement, parentElement) {
  265.         let scrollTop = targetElement.offsetTop - parentElement.offsetTop;
  266.         parentElement.scrollTop = scrollTop;
  267.     }
  268.     static centerInnerContainerScrollLeft(child, innerContainer, container) {
  269.         const childRect = child.getBoundingClientRect();
  270.         const innerContainerRect = innerContainer.getBoundingClientRect();
  271.         const containerRect = container.getBoundingClientRect();
  272.         const childLeftRelativeToInner = childRect.left - innerContainerRect.left;
  273.         const containerCenter = containerRect.width / 2;
  274.         const scrollLeft = childLeftRelativeToInner - containerCenter + (childRect.width / 2);
  275.         container.scrollLeft = scrollLeft;
  276.     }
  277.     static scrollChildToTop(child, container) {
  278.         const childRect = child.getBoundingClientRect();
  279.         const containerRect = container.getBoundingClientRect();
  280.         const childTopRelativeToContainer = childRect.top - containerRect.top;
  281.         const scrollTop = container.scrollTop + childTopRelativeToContainer;
  282.         container.scrollTop = scrollTop;
  283.     }
  284.     static getElementUnderCursor() {
  285.         return document.elementFromPoint(Dom.mousePosition.x, Dom.mousePosition.y);
  286.     }
  287.     static isElementScrolledDown(element) {
  288.         return element.scrollTop >= (element.scrollHeight - element.clientHeight);
  289.     }
  290.     static isElementScrolledUp(element) {
  291.         return element.scrollTop === 0;
  292.     }
  293.     static getScrollTopProgress(element) {
  294.         return element.scrollTop / (element.scrollHeight - element.offsetHeight);
  295.     }
  296.     static onSmoothScrollEnd(container, callback) {
  297.         let isScrolling;
  298.         function onScroll() {
  299.             window.clearTimeout(isScrolling);
  300.             isScrolling = setTimeout(() => {
  301.                 container.removeEventListener('scroll', onScroll);
  302.                 callback();
  303.             }, 300);
  304.         }
  305.         container.addEventListener('scroll', onScroll, { passive: true });
  306.     }
  307.     static scrollDown(container, smooth = false) {
  308.         if (!container) {
  309.             return;
  310.         }
  311.         container.scrollTo({
  312.             top: container.scrollHeight,
  313.             behavior: smooth ? "smooth" : "auto"
  314.         });
  315.     }
  316.     static getElementValue(element) {
  317.         let result = null;
  318.         switch (element.tagName) {
  319.             case "TEXTAREA":
  320.             case "SELECT":
  321.                 result = element.value;
  322.                 break;
  323.             case "INPUT":
  324.                 switch (element.type.toLowerCase()) {
  325.                     case "text":
  326.                     case "number":
  327.                         result = element.value;
  328.                         break
  329.                     case "checkbox":
  330.                     case "radio":
  331.                         result = element.checked;
  332.                         break;
  333.                     default:
  334.                         throw new Error(`Unknown type: ${element.type}`);
  335.                 }
  336.                 break;
  337.             default:
  338.                 throw new Error(`Unknown tag: ${element.tagName}`);
  339.         }
  340.         return result;
  341.     }
  342.     static setElementPosUnderElement(element, relativeElement, topContainer, topOffset = null) {
  343.         let top = (Dom.calcElementTop(relativeElement, topContainer) +
  344.             relativeElement.getBoundingClientRect().height + (topOffset ? topOffset : 0)) + "px";
  345.         $(element).css("top", top);
  346.         $(element).css("left", Dom.calcElementLeft(relativeElement, topContainer) + "px");
  347.     }
  348.     static calcElementTop(element, container, options = null) {
  349.         options = {
  350.             offsetParent: true,
  351.             calcScrollTop: false,
  352.             ...(options || {})
  353.         }
  354.         let offsetTop = 0;
  355.         let currentElement = element;
  356.         // Цикл для накопления offsetTop до тех пор, пока не достигнем контейнера
  357.         let lastContainer = null;
  358.         while (currentElement && currentElement !== container) {
  359.             offsetTop += (lastContainer && options.calcScrollTop ? lastContainer.scrollTop * -1 : 0) + currentElement.offsetTop;
  360.             currentElement = (options.offsetParent ? currentElement.offsetParent : currentElement.parentElement);
  361.             lastContainer = currentElement;
  362.         }
  363.         // Если элемент не является потомком контейнера, вернем null
  364.         if (currentElement !== container) return null;
  365.         return offsetTop;
  366.     }
  367.     static calcElementBottom(element, container, options = null) {
  368.         options = {
  369.             offsetParent: true,
  370.             ...(options || {})
  371.         }
  372.         const top = this.calcElementTop(element, container, options);
  373.         if (top === null) {
  374.             return null;
  375.         }
  376.         return top + element.offsetHeight;
  377.     }
  378.     static calcElementLeft(element, container) {
  379.         let offsetLeft = 0;
  380.         let currentElement = element;
  381.         // Цикл для накопления offsetTop до тех пор, пока не достигнем контейнера
  382.         while (currentElement && currentElement !== container) {
  383.             offsetLeft += currentElement.offsetLeft;
  384.             currentElement = currentElement.offsetParent;
  385.         }
  386.         // Если элемент не является потомком контейнера, вернем null
  387.         if (currentElement !== container) return null;
  388.         return offsetLeft;
  389.     }
  390.     static fitElementHorizontally(element, container, containerRect = null, showUnderElement = null) {
  391.         containerRect = containerRect || container.getBoundingClientRect();
  392.         if (element.offsetLeft < 0) {
  393.             element.style.left = `0px`;
  394.             element.style.right = ``;
  395.         }
  396.         if (element.offsetLeft + element.offsetWidth > containerRect.width) {
  397.             element.style.left = ``;
  398.             element.style.right = `0px`;
  399.         }
  400.     }
  401.     static insertHtml(position, parent, html) {
  402.         parent.insertAdjacentHTML(position, html);
  403.         switch (position.toLowerCase()) {
  404.             case "beforeend":
  405.                 return parent.children[parent.children.length - 1];
  406.                 break;
  407.             case "beforebegin":
  408.                 return parent.previousElementSibling;
  409.                 break;
  410.             case "afterend":
  411.                 return parent.parentElement.children[$(parent).index() + 1];
  412.                 break;
  413.             default:
  414.                 throw new Error("Unknown: " + position);
  415.         }
  416.     }
  417. }
  418. const qs = (...args) => {
  419.     if (typeof args[0] !== 'string' && !(args[0] instanceof String)) {
  420.         return args[0].querySelector(args.slice(1));
  421.     }
  422.     return document.querySelector(...args);
  423. }
  424. const qsa = (...args) => {
  425.     if (typeof args[0] !== 'string' && !(args[0] instanceof String)) {
  426.         return args[0].querySelectorAll(args.slice(1));
  427.     }
  428.     return document.querySelectorAll(...args);
  429. };
  430. const DEBUG_MODE = true;
  431. $(() => {
  432.     document.addEventListener('mousemove', function(event) {
  433.         Dom.mousePosition.x = event.clientX;
  434.         Dom.mousePosition.y = event.clientY;
  435.     });
  436. });
  437. class DomListener {
  438.     constructor(element, observerConfig, callback) {
  439.         this.targetNode = element;
  440.         const mutationCallback = function(mutationsList, observer) {
  441.             callback(mutationsList);
  442.             // console.log('DOM has changed:', mutationsList);
  443.         };
  444.         this.observer = new MutationObserver(mutationCallback);
  445.         this.config = observerConfig; //{ childList: true, subtree: true, attributes: true }
  446.         this.observer.observe(this.targetNode, this.config);
  447.     }
  448.     disconnect = () => {
  449.         this.observer.disconnect();
  450.     }
  451. }