commit 3d2f2a62927c0bcc86968b94cda21adb90ae40a3 Author: Mirzaev Date: Mon Oct 31 12:51:11 2022 +1000 Transtition from codepen diff --git a/hotline.js b/hotline.js new file mode 100644 index 0000000..21eb4ba --- /dev/null +++ b/hotline.js @@ -0,0 +1,679 @@ +"use strict"; + +/** + * Бегущая строка + * + * @description + * Простой, но мощный класс для создания бегущих строк. Поддерживает + * перемещение мышью и прокрутку колесом, полностью настраивается очень гибок + * для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами. + * Имеет свой препроцессор, благодаря которому можно создавать бегущие строки + * без программирования - с помощью HTML-аттрибутов, а так же возможность + * изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать + * события при выбранных действиях для того, чтобы пользователь имел возможность + * дорабатывать функционал без изучения и изменения моего кода + * + * @example + * сonst hotline = new hotline(); + * hotline.step = '-5'; + * hotline.start(); + * + * @todo + * 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты) + * + * @copyright WTFPL + * @author Arsen Mirzaev Tatyano-Muradovich + */ +class hotline { + // Идентификатор + #id = 0; + + // Оболочка (instanceof HTMLElement) + #shell = document.getElementById("hotline"); + + // Инстанция горячей строки + #instance = null; + + // Перемещение + #transfer = true; + + // Движение + #move = true; + + // Наблюдатель + #observer = null; + + // Наблюдатель + #block = new Set(["events"]); + + // Настраиваемые параметры + transfer = null; + move = null; + delay = 10; + step = 1; + hover = true; + movable = true; + sticky = false; + wheel = false; + delta = null; + vertical = false; + observe = false; + events = new Map([ + ["start", false], + ["stop", false], + ["move", false], + ["move.block", false], + ["move.unblock", false], + ["offset", false], + ["transfer.start", true], + ["transfer.end", true], + ["onmousemove", false] + ]); + + constructor(id, shell) { + // Запись идентификатора + if (typeof id === "string" || typeof id === "number") this.#id = id; + + // Запись оболочки + if (shell instanceof HTMLElement) this.#shell = shell; + } + + start() { + if (this.#instance === null) { + // Нет запущенной инстанции бегущей строки + + // Инициализация ссылки на ядро + const _this = this; + + // Запуск движения + this.#instance = setInterval(function () { + if (_this.#shell.childElementCount > 1) { + // Найдено содержимое бегущей строки (2 и более) + + // Инициализация буфера для временных данных + let buffer; + + // Инициализация данных первого элемента в строке + const first = { + element: (buffer = _this.#shell.firstElementChild), + coords: buffer.getBoundingClientRect() + }; + + if (_this.vertical) { + // Вертикальная бегущая строка + + // Инициализация сдвига у первого элемента (движение) + first.offset = isNaN( + (buffer = parseFloat(first.element.style.marginTop)) + ) + ? 0 + : buffer; + + // Инициализация отступа до второго элемента у первого элемента (разделение) + first.separator = isNaN( + (buffer = parseFloat( + getComputedStyle(first.element).marginBottom + )) + ) + ? 0 + : buffer; + + // Инициализация крайнего с конца ребра первого элемента в строке + first.end = first.coords.y + first.coords.height + first.separator; + } else { + // Горизонтальная бегущая строка + + // Инициализация отступа у первого элемента (движение) + first.offset = isNaN( + (buffer = parseFloat(first.element.style.marginLeft)) + ) + ? 0 + : buffer; + + // Инициализация отступа до второго элемента у первого элемента (разделение) + first.separator = isNaN( + (buffer = parseFloat(getComputedStyle(first.element).marginRight)) + ) + ? 0 + : buffer; + + // Инициализация крайнего с конца ребра первого элемента в строке + first.end = first.coords.x + first.coords.width + first.separator; + } + + if ( + (_this.vertical && + Math.round(first.end) < _this.#shell.offsetTop) || + (!_this.vertical && Math.round(first.end) < _this.#shell.offsetLeft) + ) { + // Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки) + + if ( + (_this.transfer === null && _this.#transfer) || + _this.transfer === true + ) { + // Перенос разрешен + + if (_this.vertical) { + // Вертикальная бегущая строка + + // Удаление отступов (движения) + first.element.style.marginTop = null; + } else { + // Горизонтальная бегущая строка + + // Удаление отступов (движения) + first.element.style.marginLeft = null; + } + + // Копирование первого элемента в конец строки + _this.#shell.appendChild(first.element); + + if (_this.events.get("transfer.end")) { + // Запрошен вызов события: "перемещение в конец" + + // Вызов события: "перемещение в конец" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.transfer.end`, { + detail: { + element: first.element, + offset: -( + (_this.vertical + ? first.coords.height + : first.coords.width) + first.separator + ) + } + }) + ); + } + } + } else if ( + (_this.vertical && + Math.round(first.coords.y) > _this.#shell.offsetTop) || + (!_this.vertical && + Math.round(first.coords.x) > _this.#shell.offsetLeft) + ) { + // Передняя (движущая) граница первого элемента вышла из области видимости + + if ( + (_this.transfer === null && _this.#transfer) || + _this.transfer === true + ) { + // Перенос разрешен + + // Инициализация отступа у последнего элемента (разделение) + const separator = + (buffer = isNaN( + (buffer = parseFloat( + getComputedStyle(_this.#shell.lastElementChild)[ + _this.vertical ? "marginBottom" : "marginRight" + ] + )) + ) + ? 0 + : buffer) === 0 + ? first.separator + : buffer; + + // Инициализация координат первого элемента в строке + const coords = _this.#shell.lastElementChild.getBoundingClientRect(); + + if (_this.vertical) { + // Вертикальная бегущая строка + + // Удаление отступов (движения) + _this.#shell.lastElementChild.style.marginTop = + -coords.height - separator + "px"; + } else { + // Горизонтальная бегущая строка + + // Удаление отступов (движения) + _this.#shell.lastElementChild.style.marginLeft = + -coords.width - separator + "px"; + } + + // Копирование последнего элемента в начало строки + _this.#shell.insertBefore( + _this.#shell.lastElementChild, + first.element + ); + + // Удаление отступов у второго элемента в строке (движения) + _this.#shell.children[1].style[ + _this.vertical ? "marginTop" : "marginLeft" + ] = null; + + if (_this.events.get("transfer.start")) { + // Запрошен вызов события: "перемещение в начало" + + // Вызов события: "перемещение в начало" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.transfer.start`, { + detail: { + element: _this.#shell.lastElementChild, + offset: + (_this.vertical ? coords.height : coords.width) + + separator + } + }) + ); + } + } + } else { + // Элемент в области видимости + + if ((_this.move === null && _this.#move) || _this.move === true) { + // Движение разрешено + + // Запись новых координат сдвига + const offset = first.offset + _this.step; + + // Запись сдвига (движение) + _this.offset(offset); + + if (_this.events.get("move")) { + // Запрошен вызов события: "движение" + + // Вызов события: "движение" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.move`, { + detail: { + from: first.offset, + to: offset + } + }) + ); + } + } + } + } + }, _this.delay); + + if (this.hover) { + // Запрошена возможность останавливать бегущую строку + + // Инициализация сдвига + let offset = 0; + + // Инициализация слушателя события при перемещении элемента в бегущей строке + const listener = function (e) { + // Увеличение сдвига + offset += e.detail.offset ?? 0; + }; + + // Инициализация обработчика наведения курсора (остановка движения) + this.#shell.onmouseover = function (e) { + // Курсор наведён на бегущую строку + + // Блокировка движения + _this.#move = false; + + if (_this.events.get("move.block")) { + // Запрошен вызов события: "блокировка движения" + + // Вызов события: "блокировка движения" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.move.block`) + ); + } + + if (_this.movable) { + // Запрошена возможность двигать бегущую строку + + _this.#shell.onmousedown = function (onmousedown) { + // Курсор активирован + + // Инициализация слушателей события перемещения элемента в бегущей строке + document.addEventListener( + `hotline.${_this.#id}.transfer.start`, + listener + ); + document.addEventListener( + `hotline.${_this.#id}.transfer.end`, + listener + ); + + // Инициализация буфера для временных данных + let buffer; + + // Инициализация данных первого элемента в строке + const first = { + offset: isNaN( + (buffer = parseFloat( + _this.vertical + ? _this.#shell.firstElementChild.style.marginTop + : _this.#shell.firstElementChild.style.marginLeft + )) + ) + ? 0 + : buffer + }; + + document.onmousemove = function (onmousemove) { + // Курсор движется + + if (_this.vertical) { + // Вертикальная бегущая строка + + // Инициализация буфера местоположения + const from = _this.#shell.firstElementChild.style.marginTop; + const to = + onmousemove.pageY - + (onmousedown.pageY + offset - first.offset); + + // Движение + _this.#shell.firstElementChild.style.marginTop = to + "px"; + + if (_this.events.get("onmousemove")) { + // Запрошен вызов события: "перемещение мышью" + + // Вызов события: "перемещение мышью" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.onmousemove`, { + detail: { from, to } + }) + ); + } + } else { + // Горизонтальная бегущая строка + + // Инициализация буфера местоположения + const from = _this.#shell.firstElementChild.style.marginLeft; + const to = + onmousemove.pageX - + (onmousedown.pageX + offset - first.offset); + + // Движение + _this.#shell.firstElementChild.style.marginLeft = to + "px"; + + if (_this.events.get("onmousemove")) { + // Запрошен вызов события: "перемещение мышью" + + // Вызов события: "перемещение мышью" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.onmousemove`, { + detail: { from, to } + }) + ); + } + } + + // Запись курсора + _this.#shell.style.cursor = "grabbing"; + }; + }; + + // Перещапись событий браузера (чтобы не дёргалось) + _this.#shell.ondragstart = null; + + _this.#shell.onmouseup = function () { + // Курсор деактивирован + + // Остановка обработки движения + document.onmousemove = null; + + // Сброс сдвига + offset = 0; + + document.removeEventListener( + `hotline.${_this.#id}.transfer.start`, + listener + ); + document.removeEventListener( + `hotline.${_this.#id}.transfer.end`, + listener + ); + + // Восстановление курсора + _this.#shell.style.cursor = null; + }; + } + }; + + // Инициализация обработчика отведения курсора (остановка движения) + this.#shell.onmouseleave = function (onmouseleave) { + // Курсор отведён от бегущей строки + + if (!_this.sticky) { + // Отключено прилипание + + // Остановка обработки движения + document.onmousemove = null; + + document.removeEventListener( + `hotline.${_this.#id}.transfer.start`, + listener + ); + document.removeEventListener( + `hotline.${_this.#id}.transfer.end`, + listener + ); + + // Восстановление курсора + _this.#shell.style.cursor = null; + } + + // Сброс сдвига + offset = 0; + + // Разблокировка движения + _this.#move = true; + + if (_this.events.get("move.unblock")) { + // Запрошен вызов события: "разблокировка движения" + + // Вызов события: "разблокировка движения" + document.dispatchEvent( + new CustomEvent(`hotline.${_this.#id}.move.unblock`) + ); + } + }; + } + + if (this.wheel) { + // Запрошена возможность прокручивать колесом мыши + + // Инициализация обработчика наведения курсора (остановка движения) + this.#shell.onwheel = function (e) { + // Курсор наведён на бегущую + + // Инициализация буфера для временных данных + let buffer; + + // Перемещение + _this.offset( + (isNaN( + (buffer = parseFloat( + _this.#shell.firstElementChild.style[ + _this.vertical ? "marginTop" : "marginLeft" + ] + )) + ) + ? 0 + : buffer) + + (_this.delta === null + ? e.wheelDelta + : e.wheelDelta > 0 + ? _this.delta + : -_this.delta) + ); + }; + } + } + + if (this.observe) { + // Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки + + if (this.#observer === null) { + // Отсутствует наблюдатель + + // Инициализация ссылки на ядро + const _this = this; + + // Инициализация наблюдателя + this.#observer = new MutationObserver(function (mutations) { + for (const mutation of mutations) { + if (mutation.type === "attributes") { + // Запись параметра в инстанцию бегущей строки + _this.configure(mutation.attributeName); + } + } + + // Перезапуск бегущей строки + _this.restart(); + }); + + // Активация наблюдения + this.#observer.observe(this.#shell, { + attributes: true + }); + } + } else if (this.#observer instanceof MutationObserver) { + // Запрошено отключение наблюдения + + // Деактивация наблюдения + this.#observer.disconnect(); + + // Удаление наблюдателя + this.#observer = null; + } + + if (this.events.get("start")) { + // Запрошен вызов события: "запуск" + + // Вызов события: "запуск" + document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.start`)); + } + + return this; + } + + stop() { + // Остановка бегущей строки + clearInterval(this.#instance); + + // Удаление инстанции интервала + this.#instance = null; + + if (this.events.get("stop")) { + // Запрошен вызов события: "остановка" + + // Вызов события: "остановка" + document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`)); + } + + return this; + } + + restart() { + // Остановка бегущей строки + this.stop(); + + // Запуск бегущей строки + this.start(); + } + + configure(attribute) { + // Инициализация названия параметра + const parameter = (/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1]; + + if (typeof parameter === "string") { + // Параметр найден + + // Проверка на разрешение изменения + if (this.#block.has(parameter)) return; + + // Инициализация значения параметра + const value = this.#shell.getAttribute(attribute); + + // Инициализация буфера для временных данных + let buffer; + + // Запись параметра + this[parameter] = isNaN((buffer = parseFloat(value))) + ? value === "true" + ? true + : value === "false" + ? false + : value + : buffer; + } + + return this; + } + + offset(value) { + // Запись отступа + this.#shell.firstElementChild.style[ + this.vertical ? "marginTop" : "marginLeft" + ] = value + "px"; + + if (this.events.get("offset")) { + // Запрошен вызов события: "сдвиг" + + // Вызов события: "сдвиг" + document.dispatchEvent( + new CustomEvent(`hotline.${this.#id}.offset`, { + detail: { + to: value + } + }) + ); + } + + return this; + } + + static preprocessing(event = false) { + // Инициализация счётчиков инстанций горячей строки + const success = new Set(); + let error = 0; + + for (const element of document.querySelectorAll('*[data-hotline="true"]')) { + // Перебор элементов для инициализации бегущих строк + + if (typeof element.id === "string") { + // Найден идентификатор + + // Инициализация инстанции бегущей строки + const hotline = new this(element.id, element); + + for (const attribute of element.getAttributeNames()) { + // Перебор аттрибутов + + // Запись параметра в инстанцию бегущей строки + hotline.configure(attribute); + } + + // Запуск бегущей строки + hotline.start(); + + // Запись инстанции бегущей строки в элемент + element.hotline = hotline; + + // Запись в счётчик успешных инициализаций + success.add(hotline); + } else ++error; + } + + if (event) { + // Запрошен вызов события: "предварительная подготовка" + + // Вызов события: "предварительная подготовка" + document.dispatchEvent( + new CustomEvent(`hotline.preprocessed`, { + detail: { + success, + error + } + }) + ); + } + } +} + +document.dispatchEvent( + new CustomEvent("hotline.loaded", { + detail: { hotline } + }) +);