hotline.mjs/hotline.mjs

1826 lines
50 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @module hotline */
"use strict";
/**
* @name hotline.mjs
*
* @description
* Module for creating "hot lines"
*
* @class
* @public
*
* @example
* сonst instance = new hotline(shell);
* instance.step = '-5';
* instance.start();
*
* {@link https://git.mirzaev.sexy/mirzaev/hotline.mjs}
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export class hotline {
/**
* @name Shell
*
* @description
* Shell of elements that will be moving
*
* @type {HTMLElement}
*
* @protected
*/
#shell;
/**
* @name First
*
* @description
* The first element of `this.#shell`
* Will be reinitialized on transfer operation ("transfer.beginning", "transfer.end")
*
* @type {object}
* @property {HTMLElement} element
* @property {DOMRect} rectangle Result of `getBoundingClientRect()`
* @property {number} position Margin (px) from the left or the top (movement)
* @property {number} offset Margin (px) from the right or the bottom (gap betweem elements)
* @property {number} end Coordinate of the element end (rectangle[(x|y)] + rectangle[(width|height)] + offset)
*
* @protected
*/
#first = {};
/**
* @name Last
*
* @description
* The last element of `this.#shell`
* Will be reinitialized on transfer operation ("transfer.beginning", "transfer.end")
*
* @type {object}
* @property {HTMLElement} element
* @property {DOMRect} rectangle Result of `getBoundingClientRect()`
* @property {number} position Margin (px) from the left or the top (movement)
* @property {number} offset Margin (px) from the right or the bottom (gap betweem elements)
* @property {number} end Coordinate of the element end (rectangle[(x|y)] + rectangle[(width|height)] + offset)
*
* @protected
*/
#last = {};
/**
* @name Status
*
* @description
* Indicator of the current state of the hotline instance.
* Can contain values: "ready", "started", "stopped".
*
* @type {(string[]|null)}
*
* @protected
*/
#status = null;
/**
* @name Process
*
* @description
* Process of moving elements and handling events.
*
* Contains identifier from setInterval().
*
* @type {(number|null)}
*
* @protected
*/
#process = null;
/**
* @name Interval
*
* @description
* Time period between executions of `this.#process` in setInterval().
*
* This greatly affects the experience. Try setting the value to 5, then 0, and then 20.
* I recommend not to ignore this property and change it depending on the type of use.
*
* `this.interval = 10` with `this.step = 1` is equal to `this.interval = 5` with `this.step = 2`
* But the difference in performance between them is two times!
*
* @type {number}
*
* @public
*/
interval = 10;
/**
* @name Alive
*
* @description
* Will elements move by themselves?
*
* @type {boolean}
*
* @public
*/
alive = true;
/**
* @name Freezed
*
* @description
* Freezed movement of elements by themselves?
*
* This property is used by the system to block the movement of elements at runtime.
* For example, when hovering the mouse cursor.
*
* @type {boolean}
*
* @protected
*/
#freezed = false;
/**
* @name Moving
*
* @description
* Is the hotline instance currently moving by the user?
*
* Contain true while handling "onmousemove" or "ontouchmove" events
*
* @type {boolean}
*
* @protected
*/
#moving = "false";
/**
* @name Moving (get)
*
* @description
* Getter for `this.#moving`
*
* @return {boolean}
*
* @public
*/
get moving() {
return this.#moving;
}
/**
* @name Movable
*
* @description
* Can the user move elements by mouse and touches?
*
* @type {boolean}
*
* @public
*/
movable = true;
/**
* @name Wheel
*
* @description
* Can the user move elements by mouse wheel?
*
* @type {boolean}
* @public
*/
wheel = false;
/**
* @name Delta
*
* @description
* Delta offset of the position when processing the "wheel" event.
*
* If the value is null, "event.wheelDelta" will be used.
* If the value is zero, then you are an insane maniac.
*
* {@link https://git.mirzaev.sexy/mirzaev/hotline.mjs/issues/5}
*
* @type {(number|null)}
*
* @public
*/
delta = 30;
/**
* @name Button
*
* @description
* Identifier of the mouse button that will perform the movement.
*
* 0: Main button pressed, usually the left button or the un-initialized state
* 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
* 2: Secondary button pressed, usually the right button
* 3: Fourth button, typically the Browser Back button
* 4: Fifth button, typically the Browser Forward button
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
*
* @type {number}
*
* @public
*/
button = 0;
/**
* @name Hover
*
* @description
* Freeze movement on mouse hover?
*
* @type {boolean}
*
* @public
*/
hover = true;
/**
* @name Step
*
* @description
* Direction and speed of movement.
* To move in the opposite direction, invert the number.
*
* It should be configured at the same time as `this.interval`!
*
* Setting `this.interval = 10` with `this.step = 1` is equal to `this.interval = 5` with `this.step = 2`.
* But the difference in performance between them is two times!
*
* Setting the value to `float` will result in "lagging" at a slow speed.
*
* @type {number}
*
* @public
*/
step = 1;
/**
* @name Transfer
*
* @description
* Allowed to transfer elements from one end to another?
*
* @type {boolean}
*
* @public
*/
transfer = true;
/**
* @name Transfer
*
* @description
* Allowed to transfer elements from one end to another?
*
* Blocks transfer even if `this.transfer === true`
*
* This is a system constant that is not overwritten in the code.
*
* @type {boolean}
*
* @protected
*/
#transfer = true;
/**
* @name Sticky
*
* @description
* Do not remove `mousemove` event listener when mouse moves out from `this.#shell`?
*
* Used to fix a bug where if you click on an element ("mousedown")
* and move the cursor outside `this.#shell` element, the functions
* for moving elements will not be removed from the "mousemove" listener.
* Clicking again to dispatch "mouseup" event does not work.
*
* Another fix is `this.#shell.addEventListener("mouseleave", leaved)`.
* But `sticky === true` is still very problematic.
*
* {@link https://git.mirzaev.sexy/mirzaev/hotline.mjs/issues/7}
*
* @type {boolean}
*
* @public
*/
sticky = false;
/**
* @name Magnetism
*
* @description
* Types of areas in which the target element will be magnetized
*
* @type {object}
*
* @typedef magnetism
* @param {symbol} beginning
* @param {symbol} center
* @param {symbol} end
*
* @protected
*/
#magnetism = Object.freeze({
beginning: Symbol("beginning"),
center: Symbol("center"),
end: Symbol("end")
});
/**
* @name Magnetism (get)
*
* @description
* Getter for `this.#magnetism`
*
* @return {magnetism}
*
* @public
*/
get magnetism() {
return this.#magnetism;
}
/**
* @name Magnetic
*
* @description
* The area in which the target element will be magnetized
*
* @type {(magnetism.<symbol>|null)}
*
* @public
*/
magnetic = null;
/**
* @name Magnet
*
* @description
* The power of magnetism (otherwise `this.step` will be used)
*
* Unlike `this.step`, this value must be a positive integer (natural), that is, it does not affect the direction
*
* @type {number}
*
* @public
*/
magnet = 1;
/**
* @name Vertical
*
* @description
* The hotline instance moves vertically?
*
* @type {boolean}
*
* @public
*/
vertical = false;
/**
* @name Observe
*
* @description
* Create an observer to change public properties when "data-hotline-*" attributes change?
*
* @type {boolean}
*
* @public
*/
observe = false;
/**
* @name Observer
*
* @description
* Contains an observer instance that allows editing the properties of the hotline instance.
* by changing the value of `this.#shell` attributes.
*
* Works only when `this.observe === true`.
*
* @type {(MutationObserver|null)}
*
* @protected
*/
#observer = null;
/**
* @name Events
*
* @description
* Registry of events that will be dispatched
*
* Events "transfer.beginning" and "transfer.end" used for elements transfers!
* Set `false` only when `this.movable === false`, otherwise there will be errors when calculating coordinates!
*
* Events dispatch by the shell element (`this.#shell`)
* Events have the form: "hotline.*" ("hotline.moved.forward", "hotline.stopped")
*
* @type {Map}
*
* @public
*/
events = new Map([
["ready", false],
["started", false],
["stopped", false],
["configured", false],
["move", false],
["move.mouse", false],
["move.touch", false],
["move.freezed", false],
["move.unfreezed", false],
["moved.forward", false],
["moved.backward", false],
["offset", false],
["transfer.beginning", true],
["transfer.end", true],
["observer.started", false],
["observer.stopped", false]
]);
/**
* @name Ignored
*
* @description
* Registry of properties (by "data-hotline-*" attributes)
* that will be ignored by the preprocessor
*
* @type {Set}
*
* @protected
*/
#ignored = new Set(["events"]);
/**
* @name Listeners
*
* @description
* Registry of event listeners
*
* @type {Map}
*
* @protected
*/
#listeners = new Map();
/**
* @name Constructor
*
* @description
* Initialize a hotline instance and shell of elements
*
* @param {HTMLElement} shell Shell
* @param {boolean} [inject=false] Write the hotline instance into the shell element?
**/
constructor(shell, inject = false) {
if (shell instanceof HTMLElement) {
// Initialized the shell of elements
// Writing the shell of elements
this.#shell = shell;
// Writing the hotline instance into the shell element
if (inject) this.#shell.hotline = this;
if (this.#shell.childElementCount > 1) {
// More than 2 elements in the `this.#shell`
// Writing status of the proccess
this.#status = "ready";
if (this.events.get("ready")) {
// Requested triggering the "ready" event
// Dispatching event: "ready"
this.#shell.dispatchEvent(new CustomEvent("hotline.ready"));
}
}
}
}
/**
* @name Start
*
* @description
* Start the process of the hotline instance
*/
start() {
if (this.#process === null) {
// Not found working process of the hotline instance
// Initializing link to the instance
const instance = this;
// Creating a process
this.#process = setInterval(() => {
// Initializing the first element
instance.#first.element = instance.#shell.firstElementChild;
// Initializing shape of the first element
instance.#first.rectangle = instance.#first.element.getBoundingClientRect();
if (instance.vertical) {
// Vertical
// Initializing position of the first element (the movement is based on this property)
instance.#first.position =
parseFloat(instance.#first.element.style.marginTop) || 0;
// Initializing offset of the first element (elements are separated like this)
instance.#first.offset =
parseFloat(getComputedStyle(instance.#first.element).marginBottom) || 0;
// Initializing coordinate of the end of the first element
instance.#first.end =
instance.#first.rectangle.y +
instance.#first.rectangle.height +
instance.#first.offset;
} else {
// Horizontal
// Initializing position of the first element (the movement is based on this property)
instance.#first.position =
parseFloat(instance.#first.element.style.marginLeft) || 0;
// Initializing offset of the first element (elements are separated like this)
instance.#first.offset =
parseFloat(getComputedStyle(instance.#first.element).marginRight) || 0;
// Initializing coordinate of the end of the first element
instance.#first.end =
instance.#first.rectangle.x +
instance.#first.rectangle.width +
instance.#first.offset;
}
if (
(instance.vertical &&
Math.round(instance.#first.end) < instance.#shell.offsetTop) ||
(!instance.vertical &&
Math.round(instance.#first.end) < instance.#shell.offsetLeft)
) {
// The first element with its separator went beyond the shell
if (instance.transfer === true && instance.#transfer) {
// Transfer is requested and allowed by system
// Transfer the first element to the end of the shell
instance.#shell.appendChild(instance.#first.element);
if (instance.vertical) {
// Vertical
// Deleting position of the last (previously first) element (the movement is based on this property)
instance.#first.element.style.marginTop = null;
if (instance.events.get("transfer.end")) {
// Requested triggering the "transfer.end" event
// Dispatching event: "transfer.end"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.transfer.end", {
detail: {
element: instance.#first.element,
offset: -(instance.#first.rectangle.height + instance.#first.offset)
}
})
);
}
// Deinitializing the first element
instance.#first = {};
} else {
// Horizontal
// Deleting position of the last (previously first) element (the movement is based on this property)
instance.#first.element.style.marginLeft = null;
if (instance.events.get("transfer.end")) {
// Requested triggering the "transfer.end" event
// Dispatching event: "transfer.end"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.transfer.end", {
detail: {
element: instance.#first.element,
offset: -(instance.#first.rectangle.width + instance.#first.offset)
}
})
);
}
// Deinitializing the first element
instance.#first = {};
}
}
} else if (
(instance.vertical &&
Math.round(instance.#first.rectangle.y) > instance.#shell.offsetTop) ||
(!instance.vertical &&
Math.round(instance.#first.rectangle.x) > instance.#shell.offsetLeft)
) {
// Beginning border of first element with its separator went beyond the shell
if (instance.transfer === true && instance.#transfer) {
// Transfer is requested and allowed by system
// Initializing the last element
instance.#last.element = instance.#shell.lastElementChild;
// Initializing shape of the last element
instance.#last.rectangle = instance.#last.element.getBoundingClientRect();
// Transfer the last element to the beginning of the shell
instance.#shell.insertBefore(
instance.#last.element,
instance.#first.element
);
if (instance.vertical) {
// Vertical
// Initializing offset of the last element (elements are separated like this)
instance.#last.offset =
parseFloat(getComputedStyle(instance.#last.element).marginBottom) ||
instance.#first.offset ||
0;
if (instance.events.get("transfer.beginning")) {
// Requested triggering the "transfer.beginning" event
// Dispatching event: "transfer.beginning"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.transfer.beginning", {
detail: {
element: instance.#last.element,
offset: instance.#last.rectangle.height + instance.#last.offset
}
})
);
}
// Initializing the position of the last element with the end boundary beyond the beginning boundary of the shell
instance.#last.element.style.marginTop =
-instance.#last.rectangle.height - instance.#last.offset + "px";
// Deleting position of the second (previously first) element (the movement is based on this property)
instance.#first.element.style.marginTop = null;
// Deinitializing the first element
instance.#first = {};
} else {
// Horizontal
// Initializing offset of the last element (elements are separated like this)
instance.#last.offset =
parseFloat(getComputedStyle(instance.#last.element).marginRight) ||
instance.#first.offset ||
0;
if (instance.events.get("transfer.beginning")) {
// Requested triggering the "transfer.beginning" event
// Dispatching event: "transfer.beginning"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.transfer.beginning", {
detail: {
element: instance.#last.element,
offset: instance.#last.rectangle.width + instance.#last.offset
}
})
);
}
// Initializing the position of the last element with the end boundary beyond the beginning boundary of the shell
instance.#last.element.style.marginLeft =
-instance.#last.rectangle.width - instance.#last.offset + "px";
// Deleting position of the second (previously first) element (the movement is based on this property)
instance.#first.element.style.marginLeft = null;
// Deinitializing the first element
instance.#first = {};
}
}
} else {
// The first element is entirely inside the shell
if (this.alive === true && this.#freezed === false) {
// Movement is requested and the hotline instance is not frozen
// Moving elements
instance.move();
}
}
}, instance.interval);
if (this.hover) {
// Requested freezing the hotline instance when the user cursor is over the this.#shell
// Initializing event listener for hovering elements by the user
this.#listeners.set("hover", (hover) => {
// The user hovers the mouse cursor over `this.#shell`
// Freezing the hotline instance (stopping movement of elements by themselves)
instance.#freezed = true;
if (instance.events.get("moving.freezed")) {
// Requested triggering the "moving.freezed" event
// Dispatching event: "moving.freezed"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.moving.freezed", {
detail: { event: hover }
})
);
}
});
// Connecting event listener for hovering elements by the user
this.#shell.addEventListener("mouseover", this.#listeners.get("hover"));
} else {
// Not requested freezing the hotline instance when the user cursor is over the this.#shell
// Disconnecting event listener for hovering elements by the user
this.#shell.removeEventListener("mouseover", this.#listeners.get("hover"));
// Deinitializing event listener for hovering elements by the user
this.#listeners.delete("hover");
}
if (this.wheel) {
// Requested moving elements by the user mouse whell
// Initializing event listener for moving elements by the user mouse wheel
this.#listeners.set("wheel", (wheel) => {
// The user moves elements by the mouse wheel
if (instance.#status === "started") {
// The hotline instance is started
// Writing new position coordinate for the first element (moving)
instance.position(
(parseFloat(
instance.#shell.firstElementChild.style[
instance.vertical ? "marginTop" : "marginLeft"
]
) || 0) +
(instance.delta === null
? wheel.wheelDelta
: wheel.wheelDelta > 0
? instance.delta
: -instance.delta)
);
}
});
// Connecting event listener for moving elements by the user mouse wheel
this.#shell.addEventListener("wheel", this.#listeners.get("wheel"));
} else {
// Not requested moving elements by the user mouse whell
// Disconnecting event listener for moving elements by the user mouse wheel
this.#shell.removeEventListener("wheel", this.#listeners.get("wheel"));
// Deinitializing event listener for moving elements by the user mouse wheel
this.#listeners.delete("wheel");
}
// Initializing buffer for generating new position of the first element
let position = 0;
// Initializing event listeners for transfered elements that moved by the user (mouse, touch)
const transfer = function (event) {
// The element was transfered while moved by the user (mouse, touch)
// Generating and writing position of the first element
position += event.detail.offset ?? 0;
};
if (instance.movable) {
// Requested moving elements by the user (mouse, touch)
// Initializing event listener for starting moving elements by the user (mouse, touch)
instance.#listeners.set("move.start", (start) => {
// Elements have started to be moved by the user (mouse, touch)
if (start.type === "touchstart" || start.button === instance.button) {
// Pressing with a finger or a mouse button specified in `this.button` by the user (mouse, touch)
// Freezing the hotline instance (stopping movement of elements by themselves)
instance.#freezed = true;
if (instance.events.get("moving.freezed")) {
// Requested triggering the "moving.freezed" event
// Dispatching event: "moving.freezed"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.moving.freezed", {
detail: { event: start }
})
);
}
// Initializing the start coordinates of the movement by the user (mouse, touch)
const x = start.pageX || (start.touches && start.touches[0]?.pageX) || 0;
const y = start.pageY || (start.touches && start.touches[0]?.pageY) || 0;
// Connecting event listener for transfer elements to the beginning
instance.#shell.addEventListener("hotline.transfer.beginning", transfer);
// Connecting event listener for transfer elements to the end
instance.#shell.addEventListener("hotline.transfer.end", transfer);
// Initializing initial position
const initial = instance.#first.position;
// Initializing event listeners for moving elements by the user (cursor, touch)
instance.#listeners.set("moving", (move) => {
// The user moves elements (cursor, touch)
if (instance.#status === "started") {
// The hotline instance is started
// Writing the status that elements are currently being moved by the user
instance.#moving = true;
if (instance.vertical) {
// Vertical
// Initializing coordinate
const coordinate =
move.pageY || (move.touches && move.touches[0].pageY) || 0;
// Writing new position coordinate for the first element (moving)
instance.position(coordinate - (y + position - initial));
} else {
// Horizontal
// Initializing coordinate
const coordinate =
move.pageX || (move.touches && move.touches[0].pageX) || 0;
// Writing new position coordinate for the first element (moving)
instance.position(coordinate - (x + position - initial));
}
if (move.type === "mousemove") {
// Elements are moved by the user using the mouse
if (instance.events.get("move.mouse")) {
// Requested triggering the "move.mouse" event
// Dispatching event: "move.mouse"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.move.mouse", {
detail: { from: initial, to: instance.#first.position }
})
);
}
} else if (move.type === "touchmove") {
// Elements are moved by the user using touches
if (instance.events.get("move.touch")) {
// Requested triggering the "move.touch" event
// Dispatching event: "move.touch"
instance.#shell.dispatchEvent(
new CustomEvent("hotline.move.touch", {
detail: { from: initial, to: instance.#first.position }
})
);
}
}
}
});
// Connecting event listener for moving elements by the user mouse
document.addEventListener("mousemove", instance.#listeners.get("moving"));
// Connecting event listener for moving elements by the user touches
document.addEventListener("touchmove", instance.#listeners.get("moving"));
}
});
// Connecting event listener for starting moving elements by the user mouse
instance.#shell.addEventListener(
"mousedown",
instance.#listeners.get("move.start")
);
// Connecting event listener for starting moving elements by the user touch
instance.#shell.addEventListener(
"touchstart",
instance.#listeners.get("move.start")
);
// Initializing event listener for leaving the user cursor from the document area
instance.#listeners.set("move.leaved", () => {
// The user mouse cursor is leave the document area
// Disconnecting event listener for moving elements by the user mouse
document.removeEventListener(
"mousemove",
instance.#listeners.get("moving")
);
// Deinitializing event listener for moving elements by the user touch
instance.#listeners.delete("moving");
// Disconnecting event listener for leaving the user cursor from the document area
document.removeEventListener(
"mouseleave",
instance.#listeners.get("move.leaved")
);
});
// Connecting event listener for leaving the user cursor from the document area
document.addEventListener(
"mouseleave",
instance.#listeners.get("move.leaved")
);
// Initializing event listeners for ending moving elements by the user (mouse, touch)
instance.#listeners.set("move.end", (end) => {
// Elements have ended to be moved by the user (mouse, touch)
// Writing the status that elements are currently not being moved by the user
instance.#moving = false;
// Disconnecting event listener for moving elements by the user mouse
document.removeEventListener(
"mousemove",
instance.#listeners.get("moving")
);
// Disconnecting event listener for moving elements by the user touch
document.removeEventListener(
"touchmove",
instance.#listeners.get("moving")
);
// Deinitializing event listener for moving elements by the user touch
instance.#listeners.delete("moving");
// Reinitializing buffer for generating new position of the first element
position = 0;
// Disconnecting event listener for transfer elements to the beginning
instance.#shell.removeEventListener(
"hotline.transfer.beginning",
transfer
);
// Disconnecting event listener for transfer elements to the end
instance.#shell.removeEventListener("hotline.transfer.end", transfer);
if (instance.hover !== false || !instance.#shell.contains(end.target)) {
// Not requested freezing or not the user cursor hovered `instance.#shell`
// Unfreezing the hotline instance (starting movement of elements by themselves)
instance.#freezed = false;
if (instance.events.get("move.unfreezed")) {
// Requested triggering the "move.unfreezed" event
// Dispatching event: "move.unfreezed"
instance.#shell.dispatchEvent(new CustomEvent("hotline.move.unfreezed"));
}
}
if (instance.magnetic !== null) {
// Requested to magnetize the first element
if (end.target === instance.#shell) {
// Target is `instance.#shell`
} else {
// Target is not `instance.#shell`
// Initializing buffer of the target element
let element = end.target;
// Initializing counter of iterations
let i = 100;
while (element.parentElement !== instance.#shell && --i !== 0) {
// Search for the target element
// Writing the possible target element
element = element.parentElement;
}
if (
element instanceof HTMLElement &&
element.parentElement === instance.#shell
) {
// Initialized the target element
// Magnetizing the first element
instance.magnetize(element, instance.magnetic);
}
}
}
});
// Connecting event listeners for ending moving elements by the user mouse
instance.#shell.addEventListener(
"mouseup",
instance.#listeners.get("move.end")
);
// Connecting event listeners for ending moving elements by the user touch
instance.#shell.addEventListener(
"touchend",
instance.#listeners.get("move.end")
);
// Initializing event listeners for leaving the user mouse cursor from the shell
instance.#listeners.set("move.leave", (leave) => {
// The user mouse cursor leaved `this.#shell` area
// Reinitializing buffer for generating new position of the first element
position = 0;
if (instance.sticky === false) {
// Not requested to stick the user mouse cursor to `this.#shell`
// Writing the status that elements are currently not being moved by the user
instance.#moving = false;
// Disconnecting event listener for moving elements by the user mouse
document.removeEventListener(
"mousemove",
instance.#listeners.get("moving")
);
// Disconnecting event listener for moving elements by the user touch
document.removeEventListener(
"touchmove",
instance.#listeners.get("moving")
);
// Deinitializing event listener for moving elements by the user touch
instance.#listeners.delete("moving");
// Disconnecting event listener for transfer elements to the beginning
instance.#shell.removeEventListener(
"hotline.transfer.beginning",
transfer
);
// Disconnecting event listener for transfer elements to the end
instance.#shell.removeEventListener("hotline.transfer.end", transfer);
}
// Unfreezing the hotline instance (starting movement of elements by themselves)
instance.#freezed = false;
if (instance.events.get("move.unfreezed")) {
// Requested triggering the "move.unfreezed" event
// Dispatching event: "move.unfreezed"
instance.#shell.dispatchEvent(new CustomEvent("hotline.move.unfreezed"));
}
});
// Connecting event listener for leaving the user mouse cursor from the shell
instance.#shell.addEventListener(
"mouseleave",
instance.#listeners.get("move.leave")
);
} else {
// Not requested moving elements by the user (mouse, touch)
// Disconnecting event listener for starting moving elements by the user mouse
instance.#shell.removeEventListener(
"mousedown",
instance.#listeners.get("move.start")
);
// Disconnecting event listener for starting moving elements by the user touch
instance.#shell.removeEventListener(
"touchstart",
instance.#listeners.get("move.start")
);
// Deinitializing event listener for starting moving elements by the user (mouse, touch)
instance.#listeners.delete("move.start");
// Writing the status that elements are currently not being moved by the user
instance.#moving = false;
// Disconnecting event listener for moving elements by the user mouse
document.removeEventListener(
"mousemove",
instance.#listeners.get("moving")
);
// Disconnecting event listener for moving elements by the user touch
document.removeEventListener(
"touchmove",
instance.#listeners.get("moving")
);
// Deinitializing event listener for moving elements by the user touch
instance.#listeners.delete("moving");
// Reinitializing buffer for generating new position of the first element
position = 0;
// Disconnecting event listeners for ending moving elements by the user mouse
instance.#shell.removeEventListener(
"mouseup",
instance.#listeners.get("move.end")
);
// Disconnecting event listeners for ending moving elements by the user touch
instance.#shell.removeEventListener(
"touchend",
instance.#listeners.get("move.end")
);
// Deinitializing event listener for ending moving elements by the user (mouse, touch)
instance.#listeners.delete("move.end");
// Disconnecting event listener for leaving the user mouse cursor from the shell
instance.#shell.addEventListener(
"mouseleave",
instance.#listeners.get("move.leave")
);
// Deinitializing event listener for leaving the user mouse cursor from the shell
instance.#listeners.delete("move.leave");
}
// Writing status of the proccess
this.#status = "started";
if (instance.events.get("started")) {
// Requested triggering the "started" event
// Dispatching event: "started"
this.#shell.dispatchEvent(new CustomEvent("hotline.started"));
}
}
if (this.observe) {
// Requester observing for changing `this.#shell` attributes values
if (this.#observer === null) {
// Not initialized the observer instance
// Initializing the observer instance
this.#observer = new MutationObserver(function (mutations) {
// Detected mutation
for (const mutation of mutations) {
// Iterating over mutations
if (mutation.type === "attributes") {
// Attribute was changed
// Reinitializing property by new value of the attribute
this.configure(mutation.attributeName);
}
}
// Restarting the hotline instance
this.restart();
});
// Starting observation for attributes mutations in the `this.#shell`
this.#observer.observe(this.#shell, {
attributes: true
});
if (this.events.get("observer.started")) {
// Requested triggering the "observer.started" event
// Dispatching event: "observer.stopped"
this.#shell.dispatchEvent(
new CustomEvent("hotline.observer.started", {
detail: {
instance: this.#observer
}
})
);
}
}
} else if (this.#observer instanceof MutationObserver) {
// Not requested observing for changing `this.#shell` attributes values but found the observer instance
// Stoppingobservation for attributes mutations in the `this.#shell`
this.#observer.disconnect();
// Deleting the observer instance
this.#observer = null;
if (this.events.get("observer.stopped")) {
// Requested triggering the "observer.stopped" event
// Dispatching event: "observer.stopped"
this.#shell.dispatchEvent(new CustomEvent("hotline.observer.stopped"));
}
}
}
/**
* @name Stop
*
* @description
* Stop the process of the hotline instance
*/
stop() {
// Stopping the process
clearInterval(this.#process);
// Deleting identifier of the proccess
this.#process = null;
// Writing status of the proccess
this.#status = "stopped";
if (this.events.get("stopped")) {
// Requested triggering the "stopped" event
// Dispatching event: "stopped"
this.#shell.dispatchEvent(new CustomEvent("hotline.stopped"));
}
}
/**
* @name Restart
*
* @description
* Stop and start the process of the hotline instance
*/
restart() {
// Stopping the hotline instance
this.stop();
// Starting the hotline instance
this.start();
}
/**
* @name Configure
*
* @description
* Validate attribute and write parameter with its value
*
* @param {string} attribute HTMLElement attribute
*/
configure(attribute) {
// Initializing parameter name
const name = (/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
if (typeof name === "string") {
// Validated parameter name
// Is the parameter allowed to be change?
if (this.#ignored.has(name)) return;
// Initializing parameter value
const value = this.#shell.getAttribute(attribute);
if (name === "magnetic" && typeof this.magnetism[value] === "symbol") {
// Validated magnetism area value
// Writing implemented value to the parameter
this.magnetic = this.magnetism[value];
} else if (typeof value === "string") {
// Validated parameter value
if (value === "true" || value === "on" || value === "yes") {
// True
// Writing implemented value to the parameter
this[name] = true;
} else if (value === "false" || value === "off" || value === "no") {
// False
// Writing implemented value to the parameter
this[name] = false;
} else {
// Number or string
// Writing value to the parameter
this[name] = parseFloat(value) || value;
}
if (this.events.get("configured")) {
// Requested triggering the "configured" event
// Dispatching event: "configured"
this.#shell.dispatchEvent(
new CustomEvent("hotline.configured", {
detail: {
name,
value: this[name]
}
})
);
}
}
}
}
/**
* @name Position
*
* @description
* Write position of the first element (margin to the left or the top)
*
* This method is used to move elements.
*
* @param {number} value Coordinate
*
* @return {number|null} Offset of new position from old position of the first element
*/
position(value) {
// Initializing old position of the first element
const old = this.#first.position || undefined;
if (typeof this.#first.element === "undefined") {
// Not initialized the first element
// Initializing the first element
this.#first.element = this.#shell.firstElementChild;
}
if (this.#first.element instanceof HTMLElement) {
// Initialized the first element
// Writing new position of the first element to the property
this.#first.position = value;
// Writing new position of the first element to the element
this.#first.element.style[this.vertical ? "marginTop" : "marginLeft"] =
this.#first.position + "px";
if (this.events.get("position")) {
// Requested triggering the "position" event
// Dispatching event: "position"
this.#shell.dispatchEvent(
new CustomEvent("hotline.position", {
detail: {
from: old,
to: value
}
})
);
}
// Calculating offset of new position from old position and exit (success)
return value - (old || 0);
}
// Exit (fail)
return null;
}
/**
* @name Move
*
* @description
* Move the first element (margin to the left or the top)
*
* This method is used to move elements.
*
* @param {number} [step] step Direction and speed of movement (otherwise `this.step`)
*
* @return {number|null} Offset of new position from old position of the first element
*/
move(step) {
// Initializing obsolete position coordinate (`x` or `y` by `this.vertical`)
const obsolete = this.#first.position;
// Initializing actual position coordinate (`x` or `y` by `this.vertical`)
const coordinate = this.#first.position + (step || this.step);
// Writing new position coordinate to the first element (moving)
const moved = this.position(coordinate);
if (this.events.get("moving")) {
// Requested triggering the "moving" event
// Dispatching event: "moving"
document.dispatchEvent(
new CustomEvent("hotline.moving", {
detail: {
from: obsolete,
to: coordinate
}
})
);
}
// Exit (success)
return moved;
}
/**
* @name Move forward
*
* @description
* Moving the first element forward untill the last element be transfered to the first element position
*
* This method is used to move elements.
*
* @return {Promise}
*/
forward() {
return new Promise((resolve, reject) => {
// Declaring timer of the forced stopping the moving process
let timer;
// Initializing speed of movement
let step = Math.abs(this.step) || 1;
// Starting moving proccess
const moving = setInterval(() => {
// Increasing the speed of movement with each iteration
++step;
// Moving
this.move(step);
}, this.interval);
// Initializing function for event listener for stopping movement when the last element be transferred to the first element position
const stopping = () => {
if (step > 10) {
// More than 10 pixels have been passed (used to fix the 1 pixel movement bug)
// Deinitializingt the moving process
clearInterval(moving);
// Deinitializing the timer of the forced stopping the moving process
clearTimeout(timer);
if (this.events.get("moved.forward")) {
// Requested triggering the "moved.forward" event
// Dispatching event: "moved.forward"
this.#shell.dispatchEvent(new CustomEvent("hotline.moved.forward"));
}
// Disconnecting event listener for stopping movement when the last element be transferred to the first element position
this.#shell.removeEventListener("hotline.transfer.beginning", stopping);
// Exit (success)
resolve();
}
};
// Connecting event listener for stopping movement when the last element be transferred to the first element position
this.#shell.addEventListener("hotline.transfer.beginning", stopping, false);
// Initializing timer of the forced stopping the moving process
timer = setTimeout(() => {
// Deinitializing the moving process
clearTimeout(moving);
// Exit (fail)
reject();
}, 5000);
});
}
/**
* @name Move backward
*
* @description
* Moving the first element backward untill the first element be transfered to the last element position
*
* This method is used to move elements.
*
* @return {Promise}
*/
backward() {
return new Promise((resolve, reject) => {
// Declaring timer of the forced stopping the moving process
let timer;
// Initializing speed of movement
let step = -Math.abs(this.step) || -1;
// Starting moving proccess
const moving = setInterval(() => {
// Increasing the speed of movement with each iteration
--step;
// Moving
this.move(step);
}, this.interval);
// Initializing function for event listener for stopping movement when the first element be transferred to the last element position
const stopping = () => {
if (step < -10) {
// More than 10 pixels have been passed (used to fix the 1 pixel movement bug)
// Deinitializingt the moving process
clearInterval(moving);
// Deinitializing the timer of the forced stopping the moving process
clearTimeout(timer);
if (this.events.get("moved.backward")) {
// Requested triggering the "moved.backward" event
// Dispatching event: "moved.backward"
this.#shell.dispatchEvent(new CustomEvent("hotline.moved.backward"));
}
// Disconnecting event listener for stopping movement when the first element be transferred to the last element position
this.#shell.removeEventListener("hotline.transfer.end", stopping);
// Exit (success)
resolve();
}
};
// Connecting event listener for stopping movement when the first element be transferred to the last element position
this.#shell.addEventListener("hotline.transfer.end", stopping, false);
// Initializing timer of the forced stopping the moving process
timer = setTimeout(() => {
// Deinitializing the moving process
clearTimeout(moving);
// Exit (fail)
reject();
}, 5000);
});
}
/**
* @name Magnetize
*
* @description
* Move the target element to the specified area and stop
*
* This method is used to move elements.
*
* To use this method you must have a fixed size of `this.#shell`,
* otherwise `this.#shell.getBoundingClientRect()` will return the result
* of the entire length including hidden elements.
*
* ⚠️ At the moment, the element does not always stopped exactly in the desired position.
* The error in getting into the desired area depends on the settings.
* If you want to stop elements for users to view, then use `this.forward` and `this.backward`,
* setting event listeners on them to implement the "swipe right" and "swipe left" technology,
* as well as the "keydown" for the "<-" and "->" buttons on the keyboard.
*
* @param {HTMLElement} element Target element that will be magnetized
* @param {magnetism.<symbol>} magnetism Magnetism area
*
* @return {Promise}
*/
magnetize(element, magnetism) {
return new Promise((resolve, reject) => {
if (element instanceof HTMLElement) {
// Initialized the element
// Initializing shape of the target element
const target = element.getBoundingClientRect();
// Initializing shape of the shell element
const shell = this.#shell.getBoundingClientRect();
// Declaring offset center of the target element from center of the shell element
let offset;
switch (magnetism) {
case this.#magnetism.beginning:
// Beginning area
// wait for updates
break;
case this.#magnetism.center:
// Central area
// Calculating and writing offset of the target element center from the shell element center
offset = target.x + target.width / 2 - (shell.x + shell.width / 2);
break;
case this.#magnetism.end:
// End area
// wait for updates
break;
default:
return;
}
if (offset > 0) {
// The target element center is ahead of the shell element center (right or bottom by `this.vertical`)
// Declaring timer of the forced stopping the magnetizing process
let timer;
// Initializing speed of movement
let step = -Math.abs(this.magnet) || -Math.abs(this.step) || 0;
// Starting magnetizing proccess (moving)
const magnet = setInterval(() => {
// Increasing the speed of movement with each iteration
--step;
// Calculating brake
const brake = offset + step;
if (brake <= 0) {
// The target element center is going to pass the shell element center
// Changing step length to reach the shell element center
step = -offset;
}
// Moving
offset += this.move(step) || 0;
if (offset === 0) {
// The target element center has reached the shell element center
// Deinitializingt the magnetizing process
clearInterval(magnet);
// Deinitializing the timer of the forced stopping the magnetizing process
clearTimeout(timer);
if (this.events.get("magnetized")) {
// Requested triggering the "magnetized" event
// Dispatching event: "magnetized"
this.#shell.dispatchEvent(
new CustomEvent("hotline.magnetized", {
detail: {
magnetism: magnetism
}
})
);
}
// Exit (success)
resolve(magnetism);
}
}, this.interval);
// Initializing timer of the forced stopping the magnetizing process
timer = setTimeout(() => {
// Deinitializing the magnetizing process
clearTimeout(magnet);
// Exit (fail)
reject();
}, 5000);
} else if (offset < 0) {
// The target element center is behind of the shell element center (left or top by `this.vertical`)
// Declaring timer of the forced stopping the magnetizing process
let timer;
// Initializing speed of movement
let step = Math.abs(this.magnet) || Math.abs(this.step) || 0;
// Starting magnetizing proccess (moving)
const magnet = setInterval(() => {
// Increasing the speed of movement with each iteration
++step;
// Calculating brake
const brake = offset + step;
if (brake >= 0) {
// The target element center is going to pass the shell element center
// Changing step length to reach the shell element center
step = -offset;
}
// Moving
offset += this.move(step) || 0;
if (offset === 0) {
// The target element center has reached the shell element center
// Deinitializingt the magnetizing process
clearInterval(magnet);
// Deinitializing the timer of the forced stopping the magnetizing process
clearTimeout(timer);
if (this.events.get("magnetized")) {
// Requested triggering the "magnetized" event
// Dispatching event: "magnetized"
this.#shell.dispatchEvent(
new CustomEvent("hotline.magnetized", {
detail: {
magnetism: magnetism
}
})
);
}
// Exit (success)
resolve(magnetism);
}
}, this.interval);
// Initializing timer of the forced stopping the magnetizing process
timer = setTimeout(() => {
// Deinitializing the magnetizing process
clearTimeout(magnet);
// Exit (fail)
reject();
}, 5000);
} else {
// The target element center has reached the shell element center
if (this.events.get("magnetized")) {
// Requested triggering the "magnetized" event
// Dispatching event: "magnetized"
this.#shell.dispatchEvent(
new CustomEvent("hotline.magnetized", {
detail: {
magnetism: magnetism
}
})
);
}
// Exit (success)
resolve(magnetism);
}
}
});
}
/**
* @name Preprocessing
*
* @description
* Read the DOM, identify shells and generate the hotline instances.
*
* It is a rather slow process, but very convenient.
* Personally, i do not recommend using it.
*
* @param {boolean} [event=false] Dispatch "hotline.preprocessed" event? (contains return values)
* @param {boolean} [inject=false] Write the hotline instance into the shell element?
*
* @return {Set} Generated the hotline instances
*/
static preprocessing(event = false, inject = false) {
// Initializing registry of generated the hotline instances
const generated = new Set();
// Initializing counter of errors
let error = 0;
for (const shell of document.querySelectorAll('*[data-hotline="true"]')) {
// Iterating over found shells with the hotline attributes
// Initializing the hotline instance
const instance = new this(shell, inject);
for (const attribute of shell.getAttributeNames()) {
// Iterating over the shell attributes
// Initializing property by value of the attribute
instance.configure(attribute);
}
// Starting the hotline instance
instance.start();
// Writing into registry of generated the hotline instances
generated.add(instance);
}
if (event) {
// Requested triggering the "hotline.preprocessed" event
// Dispatching event: "hotline.preprocessed"
document.dispatchEvent(new CustomEvent("hotline.preprocessed"), {
detail: {
generated
}
});
}
// Exit (success)
return generated;
}
}