UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

396 lines (381 loc) 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Sortable = void 0; var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs")); var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs")); var _cssAlter = require("../utils/css-alter.cjs"); var _domAlter = require("../utils/dom-alter.cjs"); var _domOptimize = require("../utils/dom-optimize.cjs"); var _domQuery = require("../utils/dom-query.cjs"); var _event = require("../utils/event.cjs"); var _math = require("../utils/math.cjs"); var _validation = require("../utils/validation.cjs"); var _callback = require("../modules/callback.cjs"); var _widget = require("./widget.cjs"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * @module Widgets */ /** * Configures the given element as a {@link Sortable} widget. * * The Sortable widget allows the user to reorder elements by dragging and * dropping. It works on touch devices as well. However, it does not yet * support automatic scrolling when dragging beyond edge of screen on mobile * devices. For this, you may want to use * {@link https://github.com/SortableJS/Sortable | SortableJS} instead. * * The widget should have more than one draggable item. * * **IMPORTANT:** You should not instantiate more than one {@link Sortable} * widget on a given element. Use {@link Sortable.get} to get an existing * instance if any. If there is already a widget instance, it will be destroyed! * * ----- * * You can use the following dynamic attributes or CSS properties in your * stylesheet: * * The following dynamic attributes are set on each item element: * - `data-lisn-is-draggable`: `"true"` or `"false"` (false if the item is disabled) * * ----- * * To use with auto-widgets (HTML API) (see * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following * CSS classes or data attributes are recognized: * - `lisn-sortable` class or `data-lisn-sortable` attribute set on the * container element that constitutes the sortable container * - `lisn-sortable-item` class or `data-lisn-sortable-item` attribute set on * elements that should act as the items. * * When using auto-widgets, the elements that will be used as items are * discovered in the following way: * 1. The top-level element that constitutes the widget is searched for any * elements that contain the `lisn-sortable-item` class or * `data-lisn-sortable-item` attribute. They do not have to be immediate * children of the root element. * 2. If there are no such elements, all of the immediate children of the * widget element (other than `script` and `style` elements) are taken as * the items. * * @example * ```html * <div class="lisn-sortable"> * <div class="box">Item 1</div> * <div class="box">Item 2</div> * <div class="box">Item 3</div> * <div class="box">Item 4</div> * </div> * ``` */ class Sortable extends _widget.Widget { static get(element) { const instance = super.get(element, DUMMY_ID); if (MH.isInstanceOf(instance, Sortable)) { return instance; } return null; } static register() { (0, _widget.registerWidget)(WIDGET_NAME, (element, config) => { if (!Sortable.get(element)) { return new Sortable(element, config); } return null; }, configValidator); } /** * @throws {@link Errors.LisnUsageError | LisnUsageError} * If there are less than 2 items given or found. */ constructor(element, config) { var _Sortable$get; const destroyPromise = (_Sortable$get = Sortable.get(element)) === null || _Sortable$get === void 0 ? void 0 : _Sortable$get.destroy(); super(element, { id: DUMMY_ID }); /** * Disables the given item number. Note that item numbers start at 1. * * @param currentOrder If false (default), the item numbers refer to the * original order. If true, they refer to the current * document order. */ _defineProperty(this, "disableItem", void 0); /** * Re-enables the given item number. Note that item numbers start at 1. * * @param currentOrder If false (default), the item numbers refer to the * original order. If true, they refer to the current * document order. */ _defineProperty(this, "enableItem", void 0); /** * Re-enables the given item number if it is disabled, otherwise disables it. * Note that item numbers start at 1. * * @param currentOrder If false (default), the item numbers refer to the * original order. If true, they refer to the current * document order. */ _defineProperty(this, "toggleItem", void 0); /** * Returns true if the given item number is disabled. Note that item numbers * start at 1. * * @param currentOrder If false (default), the item numbers refer to the * original order. If true, they refer to the current * document order. */ _defineProperty(this, "isItemDisabled", void 0); /** * The given handler will be called whenever the user moves an item to * another position. It will be called after the item is moved so * {@link getItems} called with `currentOrder = true` will return the updated * order. * * If the handler returns a promise, it will be awaited upon. */ _defineProperty(this, "onMove", void 0); /** * Returns the item elements. * * @param currentOrder If false (default), returns the items in the * original order. If true, they are returned in the * current document order. */ _defineProperty(this, "getItems", void 0); const items = (config === null || config === void 0 ? void 0 : config.items) || []; if (!MH.lengthOf(items)) { items.push(...MH.querySelectorAll(element, (0, _widget.getDefaultWidgetSelector)(PREFIX_ITEM__FOR_SELECT))); if (!MH.lengthOf(items)) { items.push(...MH.querySelectorAll(element, `[${MC.S_DRAGGABLE}]`)); if (!MH.lengthOf(items)) { items.push(...(0, _domQuery.getVisibleContentChildren)(element)); } } } if (MH.lengthOf(items) < 2) { throw MH.usageError("Sortable must have more than 1 item"); } const methods = getMethods(this, items, config); (destroyPromise || MH.promiseResolve()).then(() => { if (this.isDestroyed()) { return; } init(this, element, items, methods); }); this.disableItem = methods._disableItem; this.enableItem = methods._enableItem; this.toggleItem = methods._toggleItem; this.isItemDisabled = methods._isItemDisabled; this.onMove = methods._onMove; this.getItems = (currentOrder = false) => currentOrder ? methods._getSortedItems() : [...items]; } } /** * @interface */ exports.Sortable = Sortable; // -------------------- const WIDGET_NAME = "sortable"; const PREFIXED_NAME = MH.prefixName(WIDGET_NAME); const PREFIX_IS_DRAGGABLE = MH.prefixName("is-draggable"); // Use different classes for styling items to the one used for auto-discovering // them, so that re-creating existing widgets can correctly find the items to // be used by the new widget synchronously before the current one is destroyed. const PREFIX_ITEM = `${PREFIXED_NAME}__item`; const PREFIX_ITEM__FOR_SELECT = `${PREFIXED_NAME}-item`; const PREFIX_FLOATING_CLONE = `${PREFIXED_NAME}__ghost`; // Only one Sortable widget per element is allowed, but Widget requires a // non-blank ID. const DUMMY_ID = PREFIXED_NAME; const configValidator = { mode: (key, value) => (0, _validation.validateString)(key, value, v => v === "swap" || v === "move") }; const touchMoveOptions = { passive: false, capture: true }; const isItemDraggable = item => (0, _cssAlter.getBooleanData)(item, PREFIX_IS_DRAGGABLE); const init = (widget, element, items, methods) => { let currentDraggedItem = null; let floatingClone = null; let ignoreCancel = false; let grabOffset = [0, 0]; const setIgnoreCancel = () => ignoreCancel = true; const onDragStart = event => { const currTarget = MH.currentTargetOf(event); if (MH.isElement(currTarget) && isItemDraggable(currTarget) && MH.isMouseEvent(event)) { currentDraggedItem = currTarget; MH.setAttr(currTarget, MC.S_DRAGGABLE); if (MH.isTouchPointerEvent(event)) { const target = MH.targetOf(event); if (MH.isElement(target)) { target.releasePointerCapture(event.pointerId); } } (0, _event.addEventListenerTo)(MH.getDoc(), MC.S_TOUCHMOVE, onTouchMove, touchMoveOptions); (0, _domOptimize.waitForMeasureTime)().then(() => { // Get pointer offset relative to the current item being dragged // regardless of what the event target is and what transforms is has // applied. const rect = MH.getBoundingClientRect(currTarget); grabOffset = [event.clientX - rect.left, event.clientY - rect.top]; }); } }; const onDragEnd = event => { if (ignoreCancel && event.type === MC.S_POINTERCANCEL) { ignoreCancel = false; return; } if (currentDraggedItem) { MH.unsetAttr(currentDraggedItem, MC.S_DRAGGABLE); currentDraggedItem = null; (0, _event.removeEventListenerFrom)(MH.getDoc(), MC.S_TOUCHMOVE, onTouchMove, touchMoveOptions); if (floatingClone) { (0, _domAlter.moveElement)(floatingClone); floatingClone = null; } } }; const onTouchMove = event => { if (MH.isTouchEvent(event) && MH.lengthOf(event.touches) === 1) { const parentEl = MH.parentOf(currentDraggedItem); if (parentEl && currentDraggedItem) { MH.preventDefault(event); const touch = event.touches[0]; const clientX = touch.clientX; const clientY = touch.clientY; if (!floatingClone) { floatingClone = (0, _domAlter.cloneElement)(currentDraggedItem); (0, _cssAlter.addClasses)(floatingClone, PREFIX_FLOATING_CLONE); (0, _cssAlter.copyStyle)(currentDraggedItem, floatingClone, ["width", "height"]).then(() => { if (floatingClone) { (0, _domAlter.moveElement)(floatingClone, { to: parentEl }); } }); } if (floatingClone) { (0, _cssAlter.setNumericStyleJsVars)(floatingClone, { clientX: clientX - grabOffset[0], clientY: clientY - grabOffset[1] }, { _units: "px" }); } } } }; const onDragEnter = event => { const currTarget = MH.currentTargetOf(event); const dragged = currentDraggedItem; if ((MH.isTouchPointerEvent(event) || event.type === MC.S_DRAGENTER) && dragged && MH.isElement(currTarget) && currTarget !== dragged) { methods._dragItemOnto(dragged, currTarget); // no need to await } }; const setupEvents = () => { for (const item of items) { (0, _event.preventSelect)(item); (0, _event.addEventListenerTo)(item, MC.S_POINTERDOWN, onDragStart); (0, _event.addEventListenerTo)(item, MC.S_DRAGSTART, setIgnoreCancel); // non-touch (0, _event.addEventListenerTo)(item, MC.S_POINTERENTER, onDragEnter); // touch (0, _event.addEventListenerTo)(item, MC.S_DRAGENTER, onDragEnter); // non-touch (0, _event.addEventListenerTo)(item, MC.S_DRAGOVER, MH.preventDefault); // non-touch (0, _event.addEventListenerTo)(item, MC.S_DRAGEND, onDragEnd); // non-touch (0, _event.addEventListenerTo)(item, MC.S_DROP, onDragEnd); // non-touch (0, _event.addEventListenerTo)(MH.getDoc(), MC.S_POINTERUP, onDragEnd); (0, _event.addEventListenerTo)(MH.getDoc(), MC.S_POINTERCANCEL, onDragEnd); } }; // SETUP ------------------------------ for (const item of items) { (0, _cssAlter.addClasses)(item, PREFIX_ITEM); (0, _cssAlter.setBooleanData)(item, PREFIX_IS_DRAGGABLE); } widget.onEnable(setupEvents); widget.onDisable(() => { for (const item of items) { (0, _event.undoPreventSelect)(item); (0, _event.removeEventListenerFrom)(item, MC.S_POINTERDOWN, onDragStart); (0, _event.removeEventListenerFrom)(item, MC.S_DRAGSTART, setIgnoreCancel); (0, _event.removeEventListenerFrom)(item, MC.S_POINTERENTER, onDragEnter); (0, _event.removeEventListenerFrom)(item, MC.S_DRAGENTER, onDragEnter); (0, _event.removeEventListenerFrom)(item, MC.S_DRAGOVER, MH.preventDefault); (0, _event.removeEventListenerFrom)(item, MC.S_POINTERUP, onDragEnd); (0, _event.removeEventListenerFrom)(item, MC.S_POINTERCANCEL, onDragEnd); (0, _event.removeEventListenerFrom)(item, MC.S_DRAGEND, onDragEnd); (0, _event.removeEventListenerFrom)(item, MC.S_DROP, onDragEnd); } }); widget.onDestroy(async () => { for (const item of items) { await (0, _cssAlter.removeClasses)(item, PREFIX_ITEM); await (0, _cssAlter.delData)(item, PREFIX_IS_DRAGGABLE); } }); setupEvents(); }; const getMethods = (widget, items, config) => { const doSwap = (config === null || config === void 0 ? void 0 : config.mode) === "swap"; const disabledItems = {}; const callbacks = MH.newSet(); const getSortedItems = () => [...items].sort((a, b) => MH.isNodeBAfterA(a, b) ? -1 : 1); const getOrigItemNumber = (itemNum, currentOrder = false) => currentOrder ? items.indexOf(getSortedItems()[itemNum - 1]) + 1 : itemNum; const isItemDisabled = (itemNum, currentOrder = false) => disabledItems[getOrigItemNumber(itemNum, currentOrder)] === true; const disableItem = async (itemNum, currentOrder = false) => { itemNum = getOrigItemNumber((0, _math.toInt)(itemNum), currentOrder); if (widget.isDisabled() || itemNum < 1 || itemNum > MH.lengthOf(items)) { return; } // set immediately for toggle to work without awaiting on it disabledItems[itemNum] = true; await (0, _cssAlter.unsetBooleanData)(items[itemNum - 1], PREFIX_IS_DRAGGABLE); }; const enableItem = async (itemNum, currentOrder = false) => { itemNum = getOrigItemNumber((0, _math.toInt)(itemNum), currentOrder); if (widget.isDisabled() || !isItemDisabled(itemNum)) { return; } // set immediately for toggle to work without awaiting on it disabledItems[itemNum] = false; await (0, _cssAlter.setBooleanData)(items[itemNum - 1], PREFIX_IS_DRAGGABLE); }; const toggleItem = (itemNum, currentOrder = false) => isItemDisabled(itemNum, currentOrder) ? enableItem(itemNum, currentOrder) : disableItem(itemNum, currentOrder); const onMove = handler => callbacks.add((0, _callback.wrapCallback)(handler)); // This is internal only for now... const dragItemOnto = async (dragged, draggedOver) => { if (doSwap) { await (0, _domAlter.swapElements)(dragged, draggedOver, { ignoreMove: true }); } else { await (0, _domAlter.moveElement)(dragged, { to: draggedOver, position: MH.isNodeBAfterA(dragged, draggedOver) ? "after" : "before", ignoreMove: true }); } for (const callback of callbacks) { await callback.invoke(widget); } }; return { _getSortedItems: getSortedItems, _disableItem: disableItem, _enableItem: enableItem, _toggleItem: toggleItem, _isItemDisabled: isItemDisabled, _onMove: onMove, _dragItemOnto: dragItemOnto }; }; //# sourceMappingURL=sortable.cjs.map