class ElementViewPortVisibilityChecker {
viewPortCheckElementsData = [];
removeViewPortElementsOnScrollCheck = null;
viewPortInterval = null;
rootElement = null;
callback = null;
options = null;
isBusy = false;
constructor(rootElement = null, callback = null, options = null) {
this.options = {
calcElementOptions: {
offsetParent: true,
calcScrollTop: false,
...(options?.calcElementOptions || {})
},
...(options || {})
}
this.rootElement = rootElement;
this.callback = callback;
}
start() {
this.startViewPortElementsOnScrollCheck();
this.isBusy = false;
this.viewPortInterval = setInterval(async () => {
try {
await this.viewPortElementsCheck();
} catch (e) {
throw e;
}
}, 1000);
}
stop() {
if (this.removeViewPortElementsOnScrollCheck) {
this.removeViewPortElementsOnScrollCheck();
this.removeViewPortElementsOnScrollCheck = null;
}
if (this.viewPortInterval) {
clearInterval(this.viewPortInterval);
this.viewPortInterval = null;
}
}
clear() {
this.viewPortCheckElementsData = [];
}
startViewPortElementsOnScrollCheck() {
const onScroll = async () => {
await this.viewPortElementsCheck();
};
this.rootElement.addEventListener('scroll', onScroll);
this.removeViewPortElementsOnScrollCheck = () => {
this.rootElement.removeEventListener('scroll', onScroll);
}
}
addElement(data) {
// data = {el: null, callback: null, triggerOnce: false, ...data};
if (!this.findByElement(data.el)) {
this.viewPortCheckElementsData.push(data);
}
}
findByElement(el) {
return this.viewPortCheckElementsData.find(data => data.el === el);
}
async viewPortElementsCheck() {
if (this.isBusy) {
return;
}
this.isBusy = true;
try {
return await this._doViewPortElementsCheck();
} catch (e) {
throw e;
} finally {
this.isBusy = false;
}
}
async _doViewPortElementsCheck() {
let onCallbackTrue = (elementData) => {
if (elementData.triggerOnce) {
for (let i = 0; i < this.viewPortCheckElementsData.length; i++) { //delete
if (this.viewPortCheckElementsData[i] === elementData) {
this.viewPortCheckElementsData.splice(i, 1);
break;
}
}
}
};
let dataItems = [];
for (let data of this.viewPortCheckElementsData) {
let visibleData = Dom.getElementViewportData(data.el,
this.rootElement, {
calcElementOptions: this.options.calcElementOptions
});
if ((!data.triggerOnce || !data.isTriggered) && data.el && visibleData.isVisible) {
let dataItem = {elementData: data, visibleData};
if (this.callback) {
dataItems.push(dataItem);
} else {
if (data.callback(dataItem)) {
onCallbackTrue(data);
}
}
}
}
if (this.callback && dataItems.length) {
let callbackResult = await this.callback({dataItems: dataItems, onDataItemHandled: (dataItem, handled) => {
if (!dataItem || !dataItem.elementData) {
return;
}
if (handled) {
onCallbackTrue(dataItem.elementData);
}
}});
if (callbackResult) {
for (let dataItem of dataItems) {
onCallbackTrue(dataItem.elementData);
}
}
}
}
}
class Dom {
static viewPortCheckElementsData = [];
static mousePosition = { x: 0, y: 0 };
static onChange(target, callback, byChange = true, byKeyup = false, byProxy = false) {
function _callback(target, event = null) {
callback(target, event);
}
if (byChange) {
$(target).change(function (event) {
_callback(target, event);
});
}
if (byKeyup) {
$("[data-route-param-name]").on("keyup", function (event) {
_callback(target, event);
});
}
if (byProxy) {
const proxy = new Proxy(target, {
set(target, property, value) {
target[property] = value;
if (property === 'value') {
_callback(target, null);
}
return true;
}
});
target.proxy = proxy;
}
}
static getElementAttrs(element, attrsNames) {
let result = {};
for (let attrName of attrsNames) {
result[attrName] = $(element).attr(attrName);
}
return result;
}
static isScrolledToBottom(element) {
return element.scrollHeight - element.scrollTop === element.clientHeight;
}
static getProp(name, elementOrSelector = null) {
let element = (elementOrSelector
? (typeof elementOrSelector == 'string' ? $(elementOrSelector)[0] : elementOrSelector)
: document.documentElement);
return getComputedStyle(element).getPropertyValue(name) || document.documentElement.style.getPropertyValue(name);
}
static setProp(name, val, elementOrSelector = null) {
let element = (elementOrSelector
? (typeof elementOrSelector == 'string' ? $(elementOrSelector)[0] : elementOrSelector)
: document.documentElement);
element.style.setProperty(name, val);
}
static getMediaSize() {
let result = Dom.getProp('--media-size');
if (result) {
result = result.replaceAll(/^["]|["]$/g, "");
}
return result;
}
static findParent(element, selector = null, topParent = null) {
if (selector !== null && !Array.isArray(selector)) {
selector = [selector];
}
if (topParent !== null && !Array.isArray(topParent)) {
topParent = [topParent];
}
while (element.parentElement) {
let withSelector = (selector !== null ? selector.find(current => $(element.parentElement).is(current)) : null);
if ((selector === null || withSelector) && (!topParent || topParent.find(current => element.parentElement == current))) {
return element.parentElement;
}
element = element.parentElement;
}
}
static findParentChild(element, selector = null) {
if (selector !== null && !Array.isArray(selector)) {
selector = [selector];
}
while (element.parentElement) {
let child = null;
selector && selector.find(current => child = $(element.parentElement).find(current)[0]);
if (child) {
return child;
}
element = element.parentElement;
}
}
static getElementViewportData(el, container = null, options = null) {
options = {
calcElementOptions: {
offsetParent: true,
calcScrollTop: true,
...(options?.calcElementOptions || {})
},
...(options || {})
}
const rect = el.getBoundingClientRect();
let viewportHeight = null;
if (container) {
const containerRect = container.getBoundingClientRect();
viewportHeight = containerRect.height;
} else {
viewportHeight = window.innerHeight || document.documentElement.clientHeight;
container = window.innerHeight ? window : document.documentElement;
}
const visibleTop = Dom.calcElementTop(el, null, options.calcElementOptions);
const visibleBottom = visibleTop + rect.height;
const containerTop = container === window ? 0 : Dom.calcElementTop(container, null, options.calcElementOptions);
const containerBottom = containerTop + viewportHeight;
const visiblePixels = Math.max(0, Math.min(visibleBottom, containerBottom) - Math.max(visibleTop, containerTop));
const isFullyVisible = visiblePixels >= rect.height;
const visibilityPercentage = visiblePixels / rect.height;
const isVisible = visiblePixels > 0;
return {
visiblePixels: Math.round(visiblePixels),
portHeight: viewportHeight,
visibilityPercentage,
isFullyVisible,
isVisible,
};
}
static findClosestElement(targetY, selector = null, elements = null, elementYOffset = null) {
if (selector) {
elements = document.querySelectorAll(selector);
}
// Initialize variables to keep track of the closest element and its distance
let closestElement = null;
let closestDistance = Infinity;
// Iterate over each element
elements.forEach(element => {
// Get the bounding rectangle of the element
const rect = element.getBoundingClientRect();
// Calculate the vertical distance from the targetY to the center of the element
const elementCenterY = element.offsetTop + rect.height / 2 + (elementYOffset ? elementYOffset : 0);
const distance = Math.abs(targetY - elementCenterY);
// If this element is closer than the current closest, update the closest element
if (distance < closestDistance) {
closestDistance = distance;
closestElement = element;
}
});
return closestElement;
}
static scrollElementIntoView(targetElement, parentElement) {
let scrollTop = targetElement.offsetTop - parentElement.offsetTop;
parentElement.scrollTop = scrollTop;
}
static centerInnerContainerScrollLeft(child, innerContainer, container) {
const childRect = child.getBoundingClientRect();
const innerContainerRect = innerContainer.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const childLeftRelativeToInner = childRect.left - innerContainerRect.left;
const containerCenter = containerRect.width / 2;
const scrollLeft = childLeftRelativeToInner - containerCenter + (childRect.width / 2);
container.scrollLeft = scrollLeft;
}
static scrollChildToTop(child, container) {
const childRect = child.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const childTopRelativeToContainer = childRect.top - containerRect.top;
const scrollTop = container.scrollTop + childTopRelativeToContainer;
container.scrollTop = scrollTop;
}
static getElementUnderCursor() {
return document.elementFromPoint(Dom.mousePosition.x, Dom.mousePosition.y);
}
static isElementScrolledDown(element) {
return element.scrollTop >= (element.scrollHeight - element.clientHeight);
}
static isElementScrolledUp(element) {
return element.scrollTop === 0;
}
static getScrollTopProgress(element) {
return element.scrollTop / (element.scrollHeight - element.offsetHeight);
}
static onSmoothScrollEnd(container, callback) {
let isScrolling;
function onScroll() {
window.clearTimeout(isScrolling);
isScrolling = setTimeout(() => {
container.removeEventListener('scroll', onScroll);
callback();
}, 300);
}
container.addEventListener('scroll', onScroll, { passive: true });
}
static scrollDown(container, smooth = false) {
if (!container) {
return;
}
container.scrollTo({
top: container.scrollHeight,
behavior: smooth ? "smooth" : "auto"
});
}
static getElementValue(element) {
let result = null;
switch (element.tagName) {
case "TEXTAREA":
case "SELECT":
result = element.value;
break;
case "INPUT":
switch (element.type.toLowerCase()) {
case "text":
case "number":
result = element.value;
break
case "checkbox":
case "radio":
result = element.checked;
break;
default:
throw new Error(`Unknown type: ${element.type}`);
}
break;
default:
throw new Error(`Unknown tag: ${element.tagName}`);
}
return result;
}
static setElementPosUnderElement(element, relativeElement, topContainer, topOffset = null) {
let top = (Dom.calcElementTop(relativeElement, topContainer) +
relativeElement.getBoundingClientRect().height + (topOffset ? topOffset : 0)) + "px";
$(element).css("top", top);
$(element).css("left", Dom.calcElementLeft(relativeElement, topContainer) + "px");
}
static calcElementTop(element, container, options = null) {
options = {
offsetParent: true,
calcScrollTop: false,
...(options || {})
}
let offsetTop = 0;
let currentElement = element;
// Цикл для накопления offsetTop до тех пор, пока не достигнем контейнера
let lastContainer = null;
while (currentElement && currentElement !== container) {
offsetTop += (lastContainer && options.calcScrollTop ? lastContainer.scrollTop * -1 : 0) + currentElement.offsetTop;
currentElement = (options.offsetParent ? currentElement.offsetParent : currentElement.parentElement);
lastContainer = currentElement;
}
// Если элемент не является потомком контейнера, вернем null
if (currentElement !== container) return null;
return offsetTop;
}
static calcElementBottom(element, container, options = null) {
options = {
offsetParent: true,
...(options || {})
}
const top = this.calcElementTop(element, container, options);
if (top === null) {
return null;
}
return top + element.offsetHeight;
}
static calcElementLeft(element, container) {
let offsetLeft = 0;
let currentElement = element;
// Цикл для накопления offsetTop до тех пор, пока не достигнем контейнера
while (currentElement && currentElement !== container) {
offsetLeft += currentElement.offsetLeft;
currentElement = currentElement.offsetParent;
}
// Если элемент не является потомком контейнера, вернем null
if (currentElement !== container) return null;
return offsetLeft;
}
static fitElementHorizontally(element, container, containerRect = null, showUnderElement = null) {
containerRect = containerRect || container.getBoundingClientRect();
if (element.offsetLeft < 0) {
element.style.left = `0px`;
element.style.right = ``;
}
if (element.offsetLeft + element.offsetWidth > containerRect.width) {
element.style.left = ``;
element.style.right = `0px`;
}
}
static insertHtml(position, parent, html) {
parent.insertAdjacentHTML(position, html);
switch (position.toLowerCase()) {
case "beforeend":
return parent.children[parent.children.length - 1];
break;
case "beforebegin":
return parent.previousElementSibling;
break;
case "afterend":
return parent.parentElement.children[$(parent).index() + 1];
break;
default:
throw new Error("Unknown: " + position);
}
}
}
const qs = (...args) => {
if (typeof args[0] !== 'string' && !(args[0] instanceof String)) {
return args[0].querySelector(args.slice(1));
}
return document.querySelector(...args);
}
const qsa = (...args) => {
if (typeof args[0] !== 'string' && !(args[0] instanceof String)) {
return args[0].querySelectorAll(args.slice(1));
}
return document.querySelectorAll(...args);
};
const DEBUG_MODE = true;
$(() => {
document.addEventListener('mousemove', function(event) {
Dom.mousePosition.x = event.clientX;
Dom.mousePosition.y = event.clientY;
});
});
class DomListener {
constructor(element, observerConfig, callback) {
this.targetNode = element;
const mutationCallback = function(mutationsList, observer) {
callback(mutationsList);
// console.log('DOM has changed:', mutationsList);
};
this.observer = new MutationObserver(mutationCallback);
this.config = observerConfig; //{ childList: true, subtree: true, attributes: true }
this.observer.observe(this.targetNode, this.config);
}
disconnect = () => {
this.observer.disconnect();
}
}