hotline.mjs/hotline.js

680 lines
24 KiB
JavaScript
Raw Normal View History

2022-10-31 09:51:11 +07:00
"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 <arsen@mirzaev.sexy>
*/
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 }
})
);