@dflex/utils
Version:
Utility package for DFlex
1,744 lines (1,612 loc) • 44.6 kB
JavaScript
'use strict';
class AxesPoint {
constructor(x, y) {
this.x = x;
this.y = y;
{
Object.seal(this);
}
}
}
class Point extends AxesPoint {
/**
* Assigns the given values to the local instance.
*
* @param x
* @param y
*/
setAxes(x, y) {
this.x = x;
this.y = y;
}
/**
* Clone a given point into local instance.
*
* @param target
*/
clone(target) {
this.setAxes(target.x, target.y);
}
/**
* Get local instance of point.
*
* @returns
*/
getInstance() {
return {
x: this.x,
y: this.y
};
}
/**
* True when both axes match the same value.
*
* @param target
* @returns
*/
isInstanceEqual(target) {
return this.x === target.x && this.y === target.y;
}
/**
* True when both axes match the same value.
*
* @param x
* @param y
* @returns
*/
isEqual(x, y) {
return this.x === x && this.y === y;
}
/**
* True when both axes doesn't match the given value.
*
* @param x
* @param y
* @returns
*/
isNotEqual(x, y) {
return this.x !== x || this.y !== y;
}
}
/** Four direction instance - clockwise */
class AbstractBox {
/** Minimal `Y` coordinate */
/** Maximal `X` coordinate */
/** Maximal `Y` coordinate */
/** Minimal `X` coordinate */
/**
*
* @param top - minimal y coordinate
* @param right - maximal x coordinate
* @param bottom - maximal y coordinate
* @param left - minimal x coordinate
*/
constructor(top, right, bottom, left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
}
/** Four direction instance - clockwise */
class Box extends AbstractBox {
clone(box) {
this.top = box.top;
this.right = box.right;
this.bottom = box.bottom;
this.left = box.left;
}
/**
* Set all directions.
*
* @param top
* @param right
* @param bottom
* @param left
*/
setBox(top, right, bottom, left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
return this;
}
/**
* Get an instance of FourDirections.
*
* @returns
*/
getBox() {
return {
top: this.top,
right: this.right,
bottom: this.bottom,
left: this.left
};
}
/**
* Set one axis only.
*
* @param axis
* @param x
* @param y
*/
setByAxis(axis, x, y) {
switch (axis) {
case "x":
{
this.left = x;
this.right = y;
break;
}
default:
this.top = x;
this.bottom = y;
break;
}
}
/**
* Set one direction only.
*
* @param axis
* @param direction
* @param value
*/
setOne(axis, direction, value) {
switch (axis) {
case "x":
{
if (direction === -1) {
this.left = value;
} else {
this.right = value;
}
break;
}
default:
if (direction === -1) {
this.top = value;
} else {
this.bottom = value;
}
break;
}
}
/**
* Get the value of one direction.
*
* @param axis
* @param direction
* @returns
*/
getOne(axis, direction) {
switch (axis) {
case "x":
return direction === -1 ? this.left : this.right;
default:
return direction === -1 ? this.top : this.bottom;
}
}
setPositionInstance(point) {
this.top = point.y;
this.left = point.x;
}
setPosition(x, y) {
this.top = y;
this.left = x;
}
hasEqualPosition(x, y) {
return this.top === y || this.left === x;
}
/**
* Get starting point instance.
*
* @returns
*/
getPosition() {
return {
x: this.left,
y: this.top
};
}
}
class BoxBool extends Box {
constructor(top, right, bottom, left) {
super(top, right, bottom, left);
{
Object.seal(this);
}
}
/**
* Reset all directions to false.
*
* @returns
*/
setFalsy() {
this.setBox(false, false, false, false);
return this;
}
/**
* True when one of two directions in a given axis is true.
*
* @param axis
* @returns
*/
isTruthyByAxis(axis) {
switch (axis) {
case "x":
return this.left || this.right;
default:
return this.top || this.bottom;
}
}
isTruthyOnSide(axis, direction) {
switch (axis) {
case "x":
return direction === 1 ? this.right : this.left;
default:
return direction === 1 ? this.bottom : this.top;
}
}
/**
* True when one of four directions is true.
*
* @returns
*/
isTruthy() {
return this.left || this.right || this.top || this.bottom;
}
}
class BoxNum extends Box {
// Outside box checkers.
_isUnder(box) {
return this.top >= box.bottom;
}
_isAbove(box) {
return this.bottom <= box.top;
}
_isOnLeft(box) {
return this.right <= box.left;
}
_isOneRight(box) {
return this.left >= box.right;
}
// Outside threshold checkers.
_isAboveThresholdTop(threshold) {
return this.top < threshold.top;
}
_isRightOfThresholdRight(threshold) {
return this.right > threshold.right;
}
_isBelowThresholdBottom(threshold) {
return this.bottom > threshold.bottom;
}
_isLeftOfThresholdLeft(threshold) {
return this.left < threshold.left;
}
// Inside threshold checkers.
_isBelowOrEqualThresholdTop(threshold) {
return this.top >= threshold.top;
}
_isLeftOrEqualThresholdRight(threshold) {
return this.right <= threshold.right;
}
_isAboveOrEqualThresholdBottom(threshold) {
return this.bottom <= threshold.bottom;
}
_isRightOrEqualThresholdLeft(threshold) {
return this.left >= threshold.left;
}
isBoxIntersect(box) {
const isIntersect = !(this._isAbove(box) || this._isOneRight(box) || this._isUnder(box) || this._isOnLeft(box));
return isIntersect;
}
// isNotIntersect(box: AbstractBox): boolean {
// return !this.isBoxIntersect(box);
// }
// isOutsideBox(box: AbstractBox, outsideBox?: BoxBool): boolean {
// if (outsideBox) {
// outsideBox.setBox(false, false, false, false);
// }
// if (this._isAbove(box)) {
// if (outsideBox) {
// outsideBox.top = true;
// }
// return true;
// }
// if (this._isOneRight(box)) {
// if (outsideBox) {
// outsideBox.right = true;
// }
// return true;
// }
// if (this._isUnder(box)) {
// if (outsideBox) {
// outsideBox.bottom = true;
// }
// return true;
// }
// if (this._isOnLeft(box)) {
// if (outsideBox) {
// outsideBox.left = true;
// }
// return true;
// }
// return false;
// }
isOutThreshold(threshold, preservedBoxResult, axis) {
preservedBoxResult.setBox(false, false, false, false);
if (axis) {
if (axis === "y") {
const isAbove = this._isAboveThresholdTop(threshold);
const isBelow = this._isBelowThresholdBottom(threshold);
if (isAbove) {
preservedBoxResult.top = true;
}
if (isBelow) {
preservedBoxResult.bottom = true;
}
return isAbove || isBelow;
}
const isLeft = this._isLeftOfThresholdLeft(threshold);
const isRight = this._isRightOfThresholdRight(threshold);
if (isLeft) {
preservedBoxResult.left = true;
}
if (isRight) {
preservedBoxResult.right = true;
}
return isLeft || isRight;
}
if (this._isAboveThresholdTop(threshold)) {
preservedBoxResult.top = true;
return true;
}
if (this._isRightOfThresholdRight(threshold)) {
preservedBoxResult.right = true;
return true;
}
if (this._isBelowThresholdBottom(threshold)) {
preservedBoxResult.bottom = true;
return true;
}
if (this._isLeftOfThresholdLeft(threshold)) {
preservedBoxResult.left = true;
return true;
}
return false;
}
isInsideThreshold(threshold) {
return this._isBelowOrEqualThresholdTop(threshold) && this._isLeftOrEqualThresholdRight(threshold) && this._isAboveOrEqualThresholdBottom(threshold) && this._isRightOrEqualThresholdLeft(threshold);
}
getSurroundingBox(box) {
// Determine the coordinates of the new box
const left = Math.min(box.left, this.left);
const top = Math.min(box.top, this.top);
const right = Math.max(box.right, this.right);
const bottom = Math.max(box.bottom, this.bottom);
// Create and return the new box
return {
left,
top,
right,
bottom
};
}
isPositionedY(box) {
return this._isUnder(box) || this._isAbove(box);
}
assignBiggestBox(box) {
const {
top,
left,
right,
bottom
} = box;
if (left < this.left) {
this.left = left;
}
if (top < this.top) {
this.top = top;
}
if (right > this.right) {
this.right = right;
}
if (bottom > this.bottom) {
this.bottom = bottom;
}
}
}
function combineKeys(k1, k2) {
return `${k1}_${k2}`;
}
/* eslint-disable no-console */
const log = {};
function warnOnce(caller, ...message) {
if (!log[caller]) {
log[caller] = true;
console.warn(...message);
}
}
let didThrowError = false;
function assertElementPosition(DOM, rect) {
if (didThrowError) {
return;
}
const DOMRect = DOM.getBoundingClientRect();
const keys = Object.keys(rect);
keys.forEach(k => {
if (Object.prototype.hasOwnProperty.call(DOMRect, k) && DOMRect[k] !== rect[k]) {
didThrowError = true;
throw new Error(`Element position assertion failed. Expected: ${DOMRect[k]} found: ${rect[k]}`);
}
});
}
function getAnimationOptions(animation) {
const defaultAnimation = {
easing: "ease-in",
duration: "dynamic"
};
if (animation === undefined) {
return defaultAnimation;
}
if (animation === null) {
return null;
}
return {
...defaultAnimation,
...animation
};
}
function noop() {}
const noopSet = new Set();
{
Object.freeze(noopSet);
}
const HEIGHT = "height";
const WIDTH = "width";
const MIN_HEIGHT = "min-height";
const MIN_WIDTH = "min-width";
const LEFT = "left";
const TOP = "top";
const RIGHT = "right";
const BOTTOM = "bottom";
const POSITION = "position";
const TRANSFORM = "transform";
const OPACITY = "opacity";
function getDimensionTypeByAxis(axis) {
return axis === "x" ? WIDTH : HEIGHT;
}
function getStartingPointByAxis(axis) {
return axis === "x" ? LEFT : TOP;
}
function getEndingPointByAxis(axis) {
return axis === "x" ? RIGHT : BOTTOM;
}
function getOppositeAxis(axis) {
return axis === "x" ? "y" : "x";
}
/* eslint-disable no-dupe-class-members */
/* eslint-disable no-unused-vars */
class BoxRect extends BoxNum {
/**
* clockwise
*
* @param top
* @param right
* @param bottom
* @param left
*/
constructor(top, right, bottom, left) {
super(top, right, bottom, left);
this.width = right - left;
this.height = bottom - top;
{
Object.seal(this);
}
}
/**
*
* @param top
* @param left
* @param height
* @param width
* @returns
*/
setByPointAndDimensions(top, left, height, width) {
this.top = top;
this.left = left;
this.width = width;
this.height = height;
this.right = left + width;
this.bottom = top + height;
}
/**
* Update the box point position.
*
* @param x
* @param y
* @returns
*/
setAxes(x, y) {
this.left = x;
this.right = this.width + x;
this.top = y;
this.bottom = this.height + y;
}
getInstance() {
const {
top,
left,
bottom,
right,
width,
height
} = this;
return {
top,
left,
bottom,
right,
width,
height
};
}
/**
* Converts absolute element position to viewport position based on scroll position.
* @param viewportTop - The top position of the viewport.
* @param viewportLeft - The left position of the viewport.
* @returns The position of the element within the viewport.
*/
getViewportPos(viewportTop, viewportLeft, asBoxNum) {
const top = viewportTop;
const right = viewportLeft + this.width;
const bottom = viewportTop + this.height;
const left = viewportLeft;
return asBoxNum ? new BoxNum(top, right, bottom, left) : {
top,
right,
bottom,
left,
height: this.height,
width: this.width
};
}
/**
* Gets the width/height difference between two boxes based on axis.
*
* @param axis
* @param box
* @returns
*/
getDimensionDiff(axis, box) {
const dimensionType = getDimensionTypeByAxis(axis);
return this[dimensionType] - box[dimensionType];
}
/**
* Gets the left/top difference between two points based on axis.
*
* @param axis
* @param point
* @returns
*/
getPositionDiff(axis, point) {
const directionType = getStartingPointByAxis(axis);
return this[directionType] - point[axis];
}
}
class PointNum extends Point {
/**
* Increase the current point by the given another point.
*
* @param point
*/
increase(point) {
this.x += point.x;
this.y += point.y;
return this;
}
composeBox(box, isInner) {
const {
top,
left,
bottom,
right
} = box;
return isInner ? new BoxNum(top + this.y, right - this.x, bottom - this.y, left + this.x) : new BoxNum(top - this.y, right + this.x, bottom + this.y, left - this.x);
}
onSameAxis(axis, point) {
return axis === "y" ? point.x === this.x : point.y === this.y;
}
}
class PointBool extends Point {
/**
* True when both points X and Y are true.
* @returns
*/
isOneTruthy() {
return this.x || this.y;
}
/**
* True when one point is false.
* @returns
*/
isAllFalsy() {
return !(this.x || this.y);
}
/**
* Set both x and y to false.
*/
setFalsy() {
this.x = false;
this.y = false;
}
}
/* eslint-disable max-classes-per-file */
class DFlexThreshold {
static containerKey(depth, SK) {
return combineKeys(depth, SK);
}
static depthKey(depth) {
return combineKeys(depth, "dp");
}
constructor(percentages) {
this._percentages = percentages;
this.thresholds = {};
this.isOut = {};
}
_createPixels({
width,
height
}) {
const x = Math.round(this._percentages.horizontal * width / 100);
const y = Math.round(this._percentages.vertical * height / 100);
this._pixels = new PointNum(x, y);
}
/** Assign threshold property and create new instance for is out indicators */
_createThreshold(key, box, isInner) {
{
if (this.thresholds[key] || this.isOut[key]) {
throw new Error(`Threshold with key: ${key} already exists`);
}
}
this.thresholds[key] = this._pixels.composeBox(box, isInner);
this.isOut[key] = new BoxBool(false, false, false, false);
}
/**
* Set the main threshold for the element based on the element's dimensions
* and threshold types. For dragged and containers threshold type is outer
* `isInner=false` and for the rest of the elements `isInner=true.`
* Note: Duplicate threshold keys will throw an error.
*
* @param key
* @param box
* @param isInner
*/
setMainThreshold(key, box, isInner) {
this._createPixels(box);
this._createThreshold(key, box, isInner);
}
/**
* Update existing threshold with new dimensions.
*
* @param key
* @param rect
* @param isInner
*/
updateMainThreshold(key, rect, isInner) {
{
if (!this.thresholds[key]) {
throw new Error(`Threshold ${key} does not exist.`);
}
}
this.thresholds[key] = this._pixels.composeBox(rect, isInner);
this.isOut[key].setFalsy();
}
getElmMainThreshold(rect) {
return this._pixels.composeBox(rect, false);
}
/**
* Assign outer threshold for the container. Along with another threshold
* called insertion threshold which defines the area where the element can be
* inserted during the migration taking into consideration the biggest hight
* and width for the depth by using `unifiedContainerDimensions`. And create
* accumulated depth threshold.
*
* @param SK
* @param depth
* @param containerRect
* @param unifiedContainerDimensions
*/
setContainerThreshold(SK, depth, containerRect, unifiedContainerDimensions) {
// Regular threshold.
this._createThreshold(SK, containerRect, false);
const {
top,
left
} = containerRect;
const {
height,
width
} = unifiedContainerDimensions;
const insertionKey = DFlexThreshold.containerKey(depth, SK);
const depthKey = DFlexThreshold.depthKey(depth);
// Insertion threshold.
this._createThreshold(insertionKey, {
left,
top,
right: left + width,
bottom: top + height
}, false);
if (!this.thresholds[depthKey]) {
this._createThreshold(depthKey, this.thresholds[insertionKey], false);
return;
}
// Accumulated depth threshold. Accumulation based on insertion layer.
this.thresholds[depthKey].assignBiggestBox(this.thresholds[insertionKey]);
}
isOutThreshold(key, box, axis) {
const thresholdBox = this.thresholds[key];
return box.isOutThreshold(thresholdBox, this.isOut[key], axis);
}
destroy() {
Object.keys(this.thresholds).forEach(key => {
delete this.thresholds[key];
});
Object.keys(this.isOut).forEach(key => {
delete this.isOut[key];
});
}
}
const INITIAL_MOVEMENT = {
x: null,
y: null
};
/**
* Represents a threshold dead zone used to manage the stabilizing zone that prevents
* the dragged element from getting stuck between two intersected thresholds.
*/
class ThresholdDeadZone {
/**
* A bounding box representing the threshold dead zone.
*/
/**
* Indicates movement directions for each axis (x and y).
*/
constructor() {
this._area = new BoxNum(0, 0, 0, 0);
this._movement = {
...INITIAL_MOVEMENT
};
{
Object.seal(this);
}
}
/**
* Sets up the stabilizing zone to prevent the dragged element from getting stuck
* between two intersected thresholds.
*
* @param axis - The axis (x or y) along which the stabilizing zone is applied.
* @param movementDirection - The direction of movement on the specified axis.
* @param firstThreshold - The bounding box representing the first threshold.
* @param secondThreshold - The bounding box representing the second threshold.
*/
setZone(axis, movementDirection, firstThreshold, secondThreshold) {
// Calculate the surrounding bounding box for the stabilizing zone.
const surroundingBox = firstThreshold.getSurroundingBox(secondThreshold);
// Store the calculated area as the dead zone stabilizer.
this._area.clone(surroundingBox);
// Record the direction of movement on the specified axis.
this._movement[axis] = movementDirection;
}
/**
* Checks if the dragged element is inside the threshold dead zone.
*
* @param axis - The axis along which the movement is occurring ('x' or 'y').
* @param movementDirection - The direction of movement on the specified axis.
* @param draggedPos - The position of the dragged element.
* @returns True if the dragged element is inside the dead zone with matching
* movement direction, otherwise false.
*/
isInside(axis, movementDirection, draggedPos) {
const isInsideDeadZone = draggedPos.isInsideThreshold(this._area);
if (isInsideDeadZone) {
const hasMatchingDir = movementDirection === this._movement[axis];
return hasMatchingDir;
}
return false;
}
/**
* Clears the area and movement values, resetting them to their initial state.
*/
clear() {
this._area.setBox(0, 0, 0, 0);
this._movement = {
...INITIAL_MOVEMENT
};
}
}
class DFlexTracker {
/**
* Creates an instance of Tracker.
*/
constructor() {
this._travelID = {};
}
/**
* Increment travels and return the last one.
*/
newTravel(prefix) {
if (this._travelID[prefix] === undefined) {
this._travelID[prefix] = 0;
} else {
this._travelID[prefix] += 1;
}
return `${prefix}${this._travelID[prefix]}`;
}
}
var DFlexTrackerSingleton = (function createInstance() {
const tracker = new DFlexTracker();
return tracker;
})();
const PREFIX_TRACKER_CYCLE = "dflex_cycle_";
const PREFIX_TRACKER_ID = "dflex_id_";
const PREFIX_TRACKER_KY = "dflex_ky_";
function getSelection() {
return window.getSelection();
}
const MAX_LOOP_ELEMENTS_TO_WARN = 49;
function getParentElm(baseElement,
// eslint-disable-next-line no-unused-vars
cb) {
let iterationCounter = 0;
let current = baseElement;
try {
do {
iterationCounter += 1;
if (true) {
if (iterationCounter > MAX_LOOP_ELEMENTS_TO_WARN) {
throw new Error(`getParentElm: DFlex detected performance issues while iterating to find the nearest parent element. ` + `The element with ID ${baseElement.id} may have an excessive number of ancestors. ` + `Iteration count: ${iterationCounter}.`);
}
}
// Skip the same element `baseElement`.
if (iterationCounter > 1) {
// If the callback returns true, then we have found the parent element.
if (cb(current)) {
iterationCounter = 0;
return current;
}
}
current = current.parentElement;
} while (current !== null && !current.isSameNode(document.body));
} catch (e) {
{
// eslint-disable-next-line no-console
console.error(e);
}
} finally {
iterationCounter = 0;
}
return null;
}
function canUseDOM() {
return typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
}
function updateElmDatasetGrid(DOM, grid) {
DOM.dataset.devX = `${grid.x}`;
DOM.dataset.devY = `${grid.y}`;
}
function updateDOMAttr(DOM, name, isRemove, addPrefix = true, value = "true") {
// Keep dragged attribute as is.
const attrName = addPrefix ? `data-${name}` : name;
if (isRemove) {
{
if (!DOM.hasAttribute(attrName)) {
// eslint-disable-next-line no-console
console.error(`Attribute ${attrName} does not exist on the element.`);
}
}
DOM.removeAttribute(attrName);
return;
}
DOM.setAttribute(attrName, value);
}
function updateIndexAttr(DOM, value) {
updateDOMAttr(DOM, "index", false, true, `${value}`);
}
function getElmBoxRect(DOM, scrollLeft, scrollTop) {
const {
left,
top,
right,
bottom,
height,
width
} = DOM.getBoundingClientRect();
const boxRect = new BoxRect(top, right, bottom, left);
if (scrollLeft === 0 && scrollTop === 0) {
return boxRect;
}
/**
* Calculate the element's position by adding the scroll position to the
* left and top values obtained from getBoundingClientRect.
*/
const elementLeft = left + scrollLeft;
const elementTop = top + scrollTop;
boxRect.setByPointAndDimensions(elementTop, elementLeft, height, width);
return boxRect;
}
// eslint-disable-next-line no-unused-vars
// Maintain array of active RAF ids
const activeRAFIds = [];
// Utility to cancel RAF
function cancelRAF(rafId) {
cancelAnimationFrame(rafId);
}
// Inject completion logic into callback
function injectRAFCompleteCheck(callback, rafDone) {
return timestamp => {
callback(timestamp);
rafDone();
};
}
function DFlexCreateRAF() {
let rafId;
let isCompleted = true;
function rafDone() {
isCompleted = true;
}
function isRafDone() {
return isCompleted;
}
function cleanup() {
cancelRAF(rafId);
activeRAFIds.splice(activeRAFIds.indexOf(rafId), 1);
}
function RAF(callback, cancelPrevFrame) {
if (cancelPrevFrame) {
cleanup();
}
try {
isCompleted = false;
const wrappedCallback = injectRAFCompleteCheck(callback, rafDone);
rafId = requestAnimationFrame(wrappedCallback);
activeRAFIds.push(rafId);
} catch (error) {
{
// eslint-disable-next-line no-console
console.error(error);
}
}
}
return [RAF, cleanup, isRafDone];
}
function autoCleanupAllRAFs() {
activeRAFIds.forEach(cancelRAF);
activeRAFIds.length = 0;
}
const timeoutInstances = [];
function DFlexCreateTimeout(msDelay) {
let id = null;
let isThrottled = false;
function cleanup() {
if (id) {
clearTimeout(id);
id = null;
}
}
function timeout(callback, cancelPrevSchedule) {
const cb = callback || noop;
isThrottled = true;
if (cancelPrevSchedule) {
cleanup();
}
id = setTimeout(() => {
isThrottled = false;
cb();
}, msDelay);
}
function getIsThrottled() {
return isThrottled;
}
timeoutInstances.push(cleanup);
return [timeout, cleanup, getIsThrottled];
}
function autoCleanupAllTimeouts() {
timeoutInstances.forEach(cleanup => {
cleanup();
});
timeoutInstances.length = 0;
}
const [timeout, cancelTimeout] = DFlexCreateTimeout(0);
class TaskQueue {
constructor() {
this._queue = {};
this._elmInQueue = new Set();
}
_intiQueueRecord(queueKey) {
if (!Array.isArray(this._queue[queueKey])) {
this._queue[queueKey] = [];
}
}
_addFuncToQueueRecord(fn, queueKey, elmKey) {
this._queue[queueKey].push(fn);
if (elmKey) {
this._elmInQueue.add(elmKey);
}
}
_isEmpty(queueKey) {
return !Array.isArray(this._queue[queueKey]) || this._queue[queueKey].length === 0;
}
hasElm(elmKey) {
{
if (!this._elmInQueue.has) {
throw new Error(`The element with key ${elmKey} does not exist in the queue.`);
}
}
return this._elmInQueue.has(elmKey);
}
enqueueBeforeLast(lastElmFn, beforeLastFn, queueKey, elmKey) {
this._intiQueueRecord(queueKey);
const {
length
} = this._queue[queueKey];
if (length === 0) {
this._queue[queueKey].push(beforeLastFn);
} else {
this._queue[queueKey][length - 1] = beforeLastFn;
}
this._addFuncToQueueRecord(lastElmFn, queueKey, elmKey);
}
enqueue(fn, queueKey, elmKey) {
this._intiQueueRecord(queueKey);
this._addFuncToQueueRecord(fn, queueKey, elmKey);
}
/**
* Executes the queued tasks for the specified queue key, bypassing the scheduled execution.
*
* @param queueKey - The key of the queue to execute.
* @returns An array containing the results of executing the tasks in the queue.
*/
executeQueue(queueKey) {
const res = [];
if (this._isEmpty(queueKey)) {
return res;
}
try {
const q = this._queue[queueKey];
q.forEach(fn => {
const r = fn();
res.push(r);
});
} catch (e) {
{
// eslint-disable-next-line no-console
console.error(e);
}
} finally {
cancelTimeout();
delete this._queue[queueKey];
}
return res;
}
_schedule(keys) {
const f = () => {
const [k1, k2] = keys;
this.executeQueue(k1);
if (k2) {
queueMicrotask(() => this.executeQueue(k2));
}
this._elmInQueue.clear();
};
timeout(f, true);
}
scheduleNextTask(keys) {
this._schedule(keys);
}
// eslint-disable-next-line class-methods-use-this
cancelQueuedTask() {
cancelTimeout();
}
clear() {
cancelTimeout();
this._queue = {};
this._elmInQueue.clear();
}
}
function DFlexEventDebounce(listener, immediate = false, throttle = 200) {
const [timeout, cancelTimeout] = DFlexCreateTimeout(throttle);
const [RAF, cancelRAF] = DFlexCreateRAF();
let lastCall = performance.now();
let isPaused = false;
const debouncedListener = () => {
if (isPaused) {
return;
}
const currentTime = performance.now();
const timeSinceLastCall = currentTime - lastCall;
const shouldCallListener = immediate || timeSinceLastCall >= throttle;
if (shouldCallListener) {
// Schedule a animated frame and cancel previous one.
RAF(listener, true);
lastCall = currentTime;
} else {
// Schedule a delayed listener to be executed after the throttle period and cancel previous schedule.
timeout(debouncedListener, true);
}
};
debouncedListener.isPaused = () => isPaused;
debouncedListener.pause = () => {
if (!isPaused) {
isPaused = true;
cancelRAF();
cancelTimeout();
}
};
debouncedListener.resume = () => {
if (isPaused) {
isPaused = false;
debouncedListener();
}
};
return debouncedListener;
}
/* eslint-disable max-classes-per-file */
class AbstractDFlexCycle {
/** Transitioning element ID. */
/** Last known index for draggable before transitioning. */
/** Transition siblings key. */
/** Defined during the transition. */
/** Defined during the transition. */
constructor(index, id, SK, cycleID, hasScroll) {
this.index = index;
this.SK = SK;
this.id = id;
this.cycleID = cycleID;
this.hasScroll = hasScroll;
this.reconciledIDs = new Set();
this.numberOfTransformedELm = 0;
// TODO: Replace this with PointNum.
this.marginBottom = null;
this.marginTop = null;
{
Object.seal(this);
}
}
}
class DFlexCycle {
/** Only true when transitioning. */
/**
* Indicates whether an active drag operation is in progress.
*/
constructor(index, id, SK, cycleID, hasScroll) {
const dflexCycle = new AbstractDFlexCycle(index, id, SK, cycleID, hasScroll);
this._migrations = [dflexCycle];
this.SKs = [SK];
this.complete();
this.isActive = true;
}
/** Get the latest migrations instance */
latest() {
return this._migrations[this._migrations.length - 1];
}
/** Get the previous migrations instance */
prev() {
return this._migrations[this._migrations.length - 2];
}
getAll() {
return this._migrations;
}
/**
* Get all cycles filtered by cycleI-IDs or element-IDs.
*
* @param cycleIDs
* @param byCycleID
* @returns
*/
filter(cycleIDs, byCycleID) {
return byCycleID ? this._migrations.filter(_ => cycleIDs.find(i => i === _.cycleID)) : this._migrations.filter(_ => cycleIDs.find(i => i === _.id));
}
/**
* Delete keys from the SKs array.
*
* @param keysToDelete - A set of keys to be deleted.
*/
_deleteKeysFromSKs(keysToDelete) {
this.SKs = this.SKs.filter(key => !keysToDelete.has(key));
}
flush(cycleIDs) {
const removedKeys = new Set();
this._migrations = this._migrations.filter(_ => {
const shouldDelete = cycleIDs.find(id => {
if (id === _.SK) {
removedKeys.add(_.SK);
return true;
}
return false;
});
if (shouldDelete === undefined) {
return true;
}
if (removedKeys.has(_.SK)) {
removedKeys.delete(_.SK);
}
return false;
});
this._deleteKeysFromSKs(removedKeys);
}
pruneSKFromMigration(SK) {
this._migrations = this._migrations.filter(m => m.SK !== SK);
this._deleteKeysFromSKs(new Set([SK]));
}
/**
* We only update indexes considering migration definition when it happens
* outside container but not moving inside it.
* So we update an index but we add key.
*
* @param index
*/
setIndex(index) {
this.latest().index = index;
this.latest().numberOfTransformedELm += 1;
}
preserveVerticalMargin(type, m) {
this.latest()[type === "bottom" ? "marginBottom" : "marginTop"] = m;
}
clearMargin() {
this.latest().marginBottom = null;
this.latest().marginTop = null;
}
/**
* Add a new migration.
*
* @param index - The index of the migration.
* @param id - The ID of the element.
* @param SK - The sibling key.
* @param isAddOperation - Indicates whether the operation is an "add" operation.
* @param cycleID - The cycle ID.
* @param hasScroll - Indicates whether the element has a scroll container.
*/
add(index, id, SK, isAddOperation, cycleID, hasScroll) {
this._migrations.push(new AbstractDFlexCycle(index, id, SK, cycleID, hasScroll));
// The following logic ensures that addition operations are prioritized at
// the beginning of the array, while removal operations are placed towards
// the end. This arrangement ensures that when iterating through the array,
// you start from the most recently added containers and proceed to those
// containing omitted elements.
if (isAddOperation) {
// If it's an "add" operation, place the sibling key at the beginning of the SKs array.
this.SKs.unshift(SK);
} else {
// If it's not an "add" operation (i.e., it's a "remove" operation),
// append the sibling key to the end of the SKs array.
this.SKs.push(SK);
}
}
updateReconciledIDs(sk, reconciledIDs) {
const migration = this._migrations.find(m => m.SK === sk);
if (migration) {
migration.reconciledIDs.clear();
reconciledIDs.forEach(id => migration.reconciledIDs.add(id));
} else {
throw new Error(`Migration with SK: ${sk} not found.`);
}
}
getMigrationBySK(sk) {
return this._migrations.find(m => m.SK === sk);
}
/**
* Get reconciled IDs by sibling key (SK).
*
* @param sk - The sibling key for which to retrieve reconciled IDs.
* @returns A Set of reconciled IDs for the specified sibling key.
*/
getReconciledIDsBySK(sk) {
const migration = this._migrations.find(m => m.SK === sk);
{
if (!migration) {
throw new Error(`Migration with SK: ${sk} not found.`);
}
}
return migration ? migration.reconciledIDs : noopSet;
}
/**
* start transitioning
*/
start() {
this.isTransitioning = true;
}
/**
* Get the migration done
*/
complete() {
this.isTransitioning = false;
this.preserveVerticalMargin("top", null);
this.preserveVerticalMargin("bottom", null);
}
clear() {
this._migrations = [];
this.SKs = [];
this.isActive = false;
}
}
/** Single Axis. */
/** Bi-directional Axis. */
const BOTH_AXIS = Object.freeze(["x", "y"]);
/* eslint-disable no-redeclare */
/* eslint-disable no-shadow */
/* eslint-disable no-unused-vars */
let computedStyleCache = new WeakMap();
function parseParsedPropertyValue(value) {
const parsedValue = parseFloat(value);
return Number.isNaN(parsedValue) ? 0 : parsedValue;
}
function getCachedComputedStyle(DOM) {
if (computedStyleCache.has(DOM)) {
return computedStyleCache.get(DOM);
}
const computedStyle = getComputedStyle(DOM);
const parsedProperties = new Map();
const computedStyleCacheValue = {
computedStyle,
parsedProperties
};
computedStyleCache.set(DOM, computedStyleCacheValue);
return computedStyleCacheValue;
}
function throwIfCamelCase(str) {
if (/[a-z][A-Z]/.test(str)) {
throw new Error(`The string "${str}" is in camelCase format.`);
}
}
function verifyTypeOrThrow(value, allegedType) {
const actualType = typeof value;
if (actualType !== allegedType) {
throw new Error(`Type mismatch. Expected type ${allegedType}, but got type ${actualType}.`);
}
}
function setStyleProperty(DOM, property, value) {
DOM.style.setProperty(property, value);
}
function removeStyleProperty(DOM, property) {
DOM.style.removeProperty(property);
}
function getCachedComputedStyleProperty(DOM, property, toNumber) {
const cachedComputedStyle = getCachedComputedStyle(DOM);
const {
parsedProperties,
computedStyle
} = cachedComputedStyle;
const cachedValue = parsedProperties.get(property);
if (cachedValue === undefined) {
{
throwIfCamelCase(property);
}
const value = computedStyle.getPropertyValue(property);
const parsedPropertyValue = toNumber ? parseParsedPropertyValue(value) : value;
parsedProperties.set(property, parsedPropertyValue);
return parsedPropertyValue;
}
// Check `cachedValue` is the same type required in the call.
{
verifyTypeOrThrow(cachedValue, toNumber ? "number" : "string");
}
return cachedValue;
}
function clearComputedStyleCache() {
computedStyleCache = new WeakMap();
}
function getElmDimensions(DOM) {
{
const {
computedStyle
} = getCachedComputedStyle(DOM);
const computedWidth = computedStyle.getPropertyValue("width");
const computedHeight = computedStyle.getPropertyValue("height");
if (computedWidth.includes("%") || computedHeight.includes("%")) {
warnOnce("getElementStyle", "Element cannot have a percentage width and/or height." + "If you are expecting the element to cross multiple scroll containers, then this will cause unexpected dimension when the element is cloned.");
}
}
const width = getCachedComputedStyleProperty(DOM, WIDTH, true);
const height = getCachedComputedStyleProperty(DOM, HEIGHT, true);
return {
width,
height
};
}
function parseTransformMatrix(transform) {
const matrixPattern = /matrix\([^)]+\)/;
if (!matrixPattern.test(transform)) {
return null;
}
// Check if the transform property contains a matrix transform
const matrixMatch = transform.match(/matrix\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)\)/);
if (matrixMatch) {
const translateX = parseFloat(matrixMatch[5]);
const translateY = parseFloat(matrixMatch[6]);
return [translateX, translateY];
}
// Return null if no matrix transform is found
return null;
}
function getParsedElmTransform(DOM) {
const transform = getCachedComputedStyleProperty(DOM, TRANSFORM, false);
const transformMatrix = parseTransformMatrix(transform);
return transformMatrix;
}
// type Dimension = "width" | "height";
// const DIMENSION_PROPS: Record<Dimension, string[]> = {
// height: [
// CSSPropNames.BORDER_TOP_WIDTH,
// CSSPropNames.BORDER_BOTTOM_WIDTH,
// CSSPropNames.PADDING_TOP,
// CSSPropNames.PADDING_BOTTOM,
// ],
// width: [
// CSSPropNames.BORDER_LEFT_WIDTH,
// CSSPropNames.BORDER_RIGHT_WIDTH,
// CSSPropNames.PADDING_LEFT,
// CSSPropNames.PADDING_RIGHT,
// ],
// };
// const OFFSET_PROPS: Record<Dimension, "offsetHeight" | "offsetWidth"> = {
// height: CSSPropNames.OFFSET_HEIGHT,
// width: CSSPropNames.OFFSET_WIDTH,
// };
// function getVisibleDimension(DOM: HTMLElement, dimension: Dimension): number {
// let outerSize = 0;
// DIMENSION_PROPS[dimension].forEach((styleProp) => {
// outerSize += getCachedComputedStyleProperty(DOM, styleProp as string, true);
// });
// const totalDimension = DOM[OFFSET_PROPS[dimension]] - outerSize;
// return totalDimension;
// }
function setFixedDimensions(DOM) {
// const visibleHeight = getVisibleDimension(DOM, CSSPropNames.HEIGHT);
// const visibleWidth = getVisibleDimension(DOM, CSSPropNames.WIDTH);
const {
height,
width
} = getElmDimensions(DOM);
setStyleProperty(DOM, HEIGHT, `${height}px`);
setStyleProperty(DOM, WIDTH, `${width}px`);
}
const CSS_FORBIDDEN_POSITION_REGEX = /absolute|fixed/;
const EXPECTED_POS = "relative";
const ERROR_INVALID_POSITION = (id, actual, expected) => `setRelativePosition: Element ${id} must be positioned as relative. Found: ${actual}. Expected: ${expected}.`;
function getElmPos(DOM) {
return getCachedComputedStyleProperty(DOM, POSITION, false);
}
function getElmOverflow(DOM, overflowType) {
return getCachedComputedStyleProperty(DOM, overflowType, false);
}
function setRelativePosition(DOM) {
const position = getElmPos(DOM);
if (CSS_FORBIDDEN_POSITION_REGEX.test(position)) {
{
throw new Error(ERROR_INVALID_POSITION(DOM.id, position, EXPECTED_POS));
}
}
}
function removeOpacity(DOM) {
const opacityValue = getCachedComputedStyleProperty(DOM, OPACITY, true);
if (opacityValue !== 1) {
{
// eslint-disable-next-line no-console
console.error(`The application of opacity to the element with id '${DOM.id}' ` + `may interfere with its z-index, which is crucial for establishing the visual hierarchy. ` + `To ensure proper positioning of the drag element above others, DFlex will remove it.`);
}
setStyleProperty(DOM, OPACITY, "1");
}
}
function setParentDimensions(DOM) {
const getStyle = property => getCachedComputedStyleProperty(DOM, property, false);
const height = getStyle(HEIGHT);
const width = getStyle(WIDTH);
const minHeight = getStyle(MIN_HEIGHT);
const minWidth = getStyle(MIN_HEIGHT);
const hasMinHeight = minHeight !== "auto";
const hasMinWidth = minWidth !== "auto";
if (!hasMinHeight) {
setStyleProperty(DOM, MIN_HEIGHT, height);
}
if (!hasMinWidth) {
setStyleProperty(DOM, MIN_WIDTH, width);
}
}
function hasCSSTransition(DOM) {
const transitionValue = getCachedComputedStyleProperty(DOM, "transition", false);
return transitionValue !== "none" && transitionValue.trim() !== "";
}
function rmEmptyAttr(DOM, attribute) {
if (!DOM.hasAttribute(attribute)) {
return;
}
const value = DOM.getAttribute(attribute);
if (value && value.trim() === "") {
DOM.removeAttribute(attribute);
}
}
/**
* If true, then DFlex will assert each element position that's change and match
* with DOM.
*/
const enablePositionAssertion = false;
/**
* If true, then DFlex will override input options and reconcile changes after
* each cycle.
*/
const enableCommit = false;
const enableUndoSiblingsDebugger = false;
const enableRegisterDebugger = false;
const enableMechanismDebugger = false;
const enableScrollDebugger = false;
const enableVisibilityDebugger = false;
const enableMutationDebugger = false;
const enableReconcileDebugger = false;
var FeatureFlags = {
__proto__: null,
enableCommit: enableCommit,
enableMechanismDebugger: enableMechanismDebugger,
enableMutationDebugger: enableMutationDebugger,
enablePositionAssertion: enablePositionAssertion,
enableReconcileDebugger: enableReconcileDebugger,
enableRegisterDebugger: enableRegisterDebugger,
enableScrollDebugger: enableScrollDebugger,
enableUndoSiblingsDebugger: enableUndoSiblingsDebugger,
enableVisibilityDebugger: enableVisibilityDebugger
};
exports.AbstractBox = AbstractBox;
exports.AxesPoint = AxesPoint;
exports.BOTH_AXIS = BOTH_AXIS;
exports.Box = Box;
exports.BoxBool = BoxBool;
exports.BoxNum = BoxNum;
exports.BoxRect = BoxRect;
exports.DFlexCreateRAF = DFlexCreateRAF;
exports.DFlexCreateTimeout = DFlexCreateTimeout;
exports.DFlexCycle = DFlexCycle;
exports.PREFIX_TRACKER_CYCLE = PREFIX_TRACKER_CYCLE;
exports.PREFIX_TRACKER_ID = PREFIX_TRACKER_ID;
exports.PREFIX_TRACKER_KY = PREFIX_TRACKER_KY;
exports.Point = Point;
exports.PointBool = PointBool;
exports.PointNum = PointNum;
exports.TaskQueue = TaskQueue;
exports.Threshold = DFlexThreshold;
exports.ThresholdDeadZone = ThresholdDeadZone;
exports.assertElmPos = assertElementPosition;
exports.autoCleanupAllRAFs = autoCleanupAllRAFs;
exports.autoCleanupAllTimeouts = autoCleanupAllTimeouts;
exports.canUseDOM = canUseDOM;
exports.clearComputedStyleCache = clearComputedStyleCache;
exports.combineKeys = combineKeys;
exports.eventDebounce = DFlexEventDebounce;
exports.featureFlags = FeatureFlags;
exports.getAnimationOptions = getAnimationOptions;
exports.getCachedComputedStyleProperty = getCachedComputedStyleProperty;
exports.getDimensionTypeByAxis = getDimensionTypeByAxis;
exports.getElmBoxRect = getElmBoxRect;
exports.getElmDimensions = getElmDimensions;
exports.getElmOverflow = getElmOverflow;
exports.getElmPos = getElmPos;
exports.getEndingPointByAxis = getEndingPointByAxis;
exports.getOppositeAxis = getOppositeAxis;
exports.getParentElm = getParentElm;
exports.getParsedElmTransform = getParsedElmTransform;
exports.getSelection = getSelection;
exports.getStartingPointByAxis = getStartingPointByAxis;
exports.hasCSSTransition = hasCSSTransition;
exports.noop = noop;
exports.noopSet = noopSet;
exports.removeOpacity = removeOpacity;
exports.removeStyleProperty = removeStyleProperty;
exports.rmEmptyAttr = rmEmptyAttr;
exports.setFixedDimensions = setFixedDimensions;
exports.setParentDimensions = setParentDimensions;
exports.setRelativePosition = setRelativePosition;
exports.setStyleProperty = setStyleProperty;
exports.tracker = DFlexTrackerSingleton;
exports.updateDOMAttr = updateDOMAttr;
exports.updateElmDatasetGrid = updateElmDatasetGrid;
exports.updateIndexAttr = updateIndexAttr;
exports.warnOnce = warnOnce;