UNPKG

@progress/kendo-angular-sortable

Version:

A Sortable Component for Angular

173 lines (172 loc) 5.75 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { focusableSelector, isDocumentAvailable } from '@progress/kendo-angular-common'; const NODE_NAME_PREDICATES = {}; const NODE_ATTR_PREDICATES = {}; const focusableRegex = /^(?:a|input|select|option|textarea|button|object)$/i; /** * @hidden */ export const matchesNodeName = (nodeName) => { if (!NODE_NAME_PREDICATES[nodeName]) { NODE_NAME_PREDICATES[nodeName] = (element) => String(element.nodeName).toLowerCase() === nodeName.toLowerCase(); } return NODE_NAME_PREDICATES[nodeName]; }; /** * @hidden */ export const matchesNodeAttr = (nodeAttr) => { if (!NODE_ATTR_PREDICATES[nodeAttr]) { NODE_ATTR_PREDICATES[nodeAttr] = (element) => element.hasAttribute ? element.hasAttribute(nodeAttr) : false; } return NODE_ATTR_PREDICATES[nodeAttr]; }; /** * @hidden */ export const closest = (node, predicate) => { while (node && !predicate(node)) { node = node.parentNode; } return node; }; /** * Returns an object specifiying whether there is a DraggableDirective under the cursor. * @hidden */ export const draggableFromPoint = (x, y) => { if (!isDocumentAvailable()) { return; } const el = document.elementFromPoint(x, y); if (!el) { return; } const isDraggable = el.hasAttribute("kendoDraggable"); const isChild = closest(el, matchesNodeAttr("kendoDraggable")) !== null; const parentDraggable = closest(el, matchesNodeAttr("data-sortable-index")); const index = parentDraggable ? parseInt(parentDraggable.getAttribute("data-sortable-index"), 10) : -1; return { element: el, index: index, isDraggable: isDraggable, isDraggableChild: isChild, parentDraggable: parentDraggable, rect: el.getBoundingClientRect() }; }; /** * Returns the DraggableDirective under the cursor. * @hidden */ export const draggableFromEvent = (event, sortable) => { let target; if (event.changedTouches) { const touch = event.changedTouches[0]; target = draggableFromPoint(touch.clientX, touch.clientY); } else { target = draggableFromPoint(event.clientX, event.clientY); } // TODO: refactor sortable. Add draggable getter return sortable.draggables.toArray()[target ? target.index : -1]; }; /** * @hidden */ export const getAllFocusableChildren = (parent) => { return Array.from(parent.querySelectorAll(focusableSelector)).filter((element) => element.offsetParent !== null); }; /** * @hidden */ export const getFirstAndLastFocusable = (parent) => { const all = getAllFocusableChildren(parent); const firstFocusable = all.length > 0 ? all[0] : parent; const lastFocusable = all.length > 0 ? all[all.length - 1] : parent; return [firstFocusable, lastFocusable]; }; /** * @hidden */ export const keepFocusWithinComponent = (event, wrapper) => { const [firstFocusable, lastFocusable] = getFirstAndLastFocusable(wrapper); const tabAfterLastFocusable = !event.shiftKey && event.target === lastFocusable; const shiftTabAfterFirstFocusable = event.shiftKey && event.target === firstFocusable; if (tabAfterLastFocusable) { event.preventDefault(); firstFocusable.focus(); wrapper.blur(); } if (shiftTabAfterFirstFocusable) { event.preventDefault(); lastFocusable.focus(); } }; /** * @hidden */ export const isFocusable = (element) => { if (element.tagName) { const tagName = element.tagName.toLowerCase(); const tabIndex = element.getAttribute('tabIndex'); const skipTab = tabIndex === '-1'; let focusable = tabIndex !== null && !skipTab; if (focusableRegex.test(tagName)) { focusable = !element.disabled && !skipTab; } return focusable; } return false; }; const toClassList = (classNames) => String(classNames).trim().split(' '); /** * @hidden */ export const hasClasses = (element, classNames) => { const namesList = toClassList(classNames); return Boolean(toClassList(element.className).find((className) => namesList.indexOf(className) >= 0)); }; const isSortable = matchesNodeName('kendo-sortable'); /** * @hidden */ export const widgetTarget = (target) => { const element = closest(target, node => hasClasses(node, 'k-widget') || isSortable(node)); return element && !isSortable(element); }; const hasRelativeStackingContext = () => { if (!isDocumentAvailable()) { return false; } const top = 10; const parent = document.createElement("div"); parent.style.transform = "matrix(10, 0, 0, 10, 0, 0)"; const innerDiv = document.createElement('div'); innerDiv.style.position = 'fixed'; innerDiv.style.top = `${top}px;`; parent.appendChild(innerDiv); document.body.appendChild(parent); const isDifferent = parent.children[0].getBoundingClientRect().top !== top; document.body.removeChild(parent); return isDifferent; }; const HAS_RELATIVE_STACKING_CONTEXT = hasRelativeStackingContext(); /** * @hidden */ export const relativeContextElement = (element) => { if (!element || !HAS_RELATIVE_STACKING_CONTEXT) { return null; } let node = element.parentElement; while (node) { if (window.getComputedStyle(node).transform !== 'none') { return node; } node = node.parentElement; } };