UNPKG

@dflex/utils

Version:
1,744 lines (1,612 loc) 44.6 kB
'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;