@dflex/utils
Version:
Utility package for DFlex
1,688 lines (1,557 loc) • 43.3 kB
JavaScript
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
};
export { AbstractBox, AxesPoint, BOTH_AXIS, Box, BoxBool, BoxNum, BoxRect, DFlexCreateRAF, DFlexCreateTimeout, DFlexCycle, PREFIX_TRACKER_CYCLE, PREFIX_TRACKER_ID, PREFIX_TRACKER_KY, Point, PointBool, PointNum, TaskQueue, DFlexThreshold as Threshold, ThresholdDeadZone, assertElementPosition as assertElmPos, autoCleanupAllRAFs, autoCleanupAllTimeouts, canUseDOM, clearComputedStyleCache, combineKeys, DFlexEventDebounce as eventDebounce, FeatureFlags as featureFlags, getAnimationOptions, getCachedComputedStyleProperty, getDimensionTypeByAxis, getElmBoxRect, getElmDimensions, getElmOverflow, getElmPos, getEndingPointByAxis, getOppositeAxis, getParentElm, getParsedElmTransform, getSelection, getStartingPointByAxis, hasCSSTransition, noop, noopSet, removeOpacity, removeStyleProperty, rmEmptyAttr, setFixedDimensions, setParentDimensions, setRelativePosition, setStyleProperty, DFlexTrackerSingleton as tracker, updateDOMAttr, updateElmDatasetGrid, updateIndexAttr, warnOnce };