UNPKG

@limetech/lime-elements

Version:
354 lines (353 loc) • 13.5 kB
import React from "react"; import { isObjectType } from "../schema"; import { CollapsibleItemTemplate } from "./array-field-collapsible-item"; import { SimpleItemTemplate } from "./array-field-simple-item"; import { renderDescription, renderTitle } from "./common"; import Sortable from "sortablejs"; const DRAG_HANDLE_SELECTOR = '[data-drag-handle]'; const DRAGGABLE_ITEM_SELECTOR = '.array-item[data-reorderable="true"]'; const DEFAULT_CONTAINER_CLASS = 'has-an-item-which-is-being-dragged'; const DEFAULT_DROP_ELEVATION_CLASS = 'is-elevated'; const DROP_ELEVATION_DURATION = 1000; const TOUCH_DRAG_DELAY_MS = 200; // Adds a short hold on touch (long-press) so scroll gestures do not reorder items. export class ArrayFieldTemplate extends React.Component { constructor(props) { super(props); this.itemByIndex = new Map(); this.handleAddClick = (event) => { event.stopPropagation(); this.props.onAddClick(event); }; this.setContainer = (element) => { if (this.container === element) { return; } this.teardownDragController(); this.container = element !== null && element !== void 0 ? element : undefined; this.setupDragController(); }; this.handleSortStart = (event) => { if (!this.canReorderItems()) { return; } this.dragSnapshot = [...this.state.order]; this.draggedItemIndex = this.getReorderId(event.item); if (this.container) { this.container.classList.add(DEFAULT_CONTAINER_CLASS); } if (event.item instanceof HTMLElement) { this.applyDropElevation(event.item); } }; this.handleSortEnd = (event) => { var _a; if (!this.canReorderItems()) { return; } if (this.container) { this.container.classList.remove(DEFAULT_CONTAINER_CLASS); } const snapshot = this.dragSnapshot; const draggedItemIndex = this.draggedItemIndex; this.dragSnapshot = undefined; this.draggedItemIndex = undefined; if (event.item instanceof HTMLElement) { this.dropElevationTarget = event.item; } const finalOrder = this.readOrderFromDom(); if (!this.arraysEqual(this.state.order, finalOrder)) { this.setState({ order: finalOrder }); } if (snapshot === undefined || draggedItemIndex === undefined || !(event.item instanceof HTMLElement)) { this.scheduleDropElevationRemoval(); return; } const fromPosition = snapshot.indexOf(draggedItemIndex); const toPosition = finalOrder.indexOf(draggedItemIndex); if (fromPosition === -1 || toPosition === -1 || fromPosition === toPosition) { this.scheduleDropElevationRemoval(); return; } const targetItem = ((_a = this.props.items) !== null && _a !== void 0 ? _a : []).find((entry) => { var _a; return ((_a = entry.index) !== null && _a !== void 0 ? _a : -1) === draggedItemIndex; }); if (!targetItem || typeof targetItem.onReorderClick !== 'function') { this.scheduleDropElevationRemoval(); return; } requestAnimationFrame(() => { const reorder = targetItem.onReorderClick(draggedItemIndex, toPosition); if (typeof reorder === 'function') { reorder(); } }); this.scheduleDropElevationRemoval(); }; this.state = { order: this.extractIndices(props.items), }; } componentWillUnmount() { this.teardownDragController(); } componentDidMount() { this.setupDragController(); } componentDidUpdate(previousProps) { if (previousProps.items !== this.props.items) { const nextOrder = this.extractIndices(this.props.items); if (!this.arraysEqual(this.state.order, nextOrder)) { this.setState({ order: nextOrder, }, () => { this.setupDragController(); }); return; } } if (previousProps.items !== this.props.items || previousProps.schema !== this.props.schema || previousProps.disabled !== this.props.disabled || previousProps.readonly !== this.props.readonly) { this.setupDragController(); } } render() { const controls = this.getItemControls(); const { ordered: orderedItems, byIndex } = this.getOrderedItems(); this.itemByIndex = byIndex; return React.createElement('div', {}, renderTitle(this.props.title), renderDescription(this.props.schema.description), React.createElement('div', { className: 'array-items', ref: this.setContainer, }, orderedItems.map((item, index) => this.renderItem(item, index, controls))), this.renderAddButton()); } renderAddButton() { if (!this.props.canAdd) { return; } return React.createElement('limel-button', { label: this.props.title || 'Add', onClick: this.handleAddClick, icon: 'plus_math', class: 'button-add-new', }); } getItemControls() { return { allowItemRemoval: this.canRemoveItems(), }; } renderItem(item, index, controls) { var _a; const { schema, formData, formContext } = this.props; const itemIndex = (_a = item.index) !== null && _a !== void 0 ? _a : index; const allowItemReorder = this.isItemReorderable(item); if (isObjectType(schema.items)) { return React.createElement(CollapsibleItemTemplate, { key: item.key, item: item, data: Array.isArray(formData) ? formData[itemIndex] : undefined, schema: schema, formSchema: formContext.schema, index: itemIndex, allowItemRemoval: controls.allowItemRemoval, allowItemReorder: allowItemReorder, }); } return React.createElement(SimpleItemTemplate, { key: item.key, item: item, index: itemIndex, dataIndex: itemIndex, allowItemRemoval: controls.allowItemRemoval, allowItemReorder: allowItemReorder, }); } getOrderedItems() { var _a, _b; const items = (_a = this.props.items) !== null && _a !== void 0 ? _a : []; const byIndex = new Map(); let entryIndex = 0; for (const entry of items) { byIndex.set((_b = entry.index) !== null && _b !== void 0 ? _b : entryIndex, entry); entryIndex += 1; } const ordered = []; const used = new Set(); for (const index of this.state.order) { const entry = byIndex.get(index); if (!entry) { continue; } ordered.push(entry); used.add(index); } for (const [index, entry] of byIndex.entries()) { if (!used.has(index)) { ordered.push(entry); } } return { ordered, byIndex, }; } setupDragController() { if (!this.container || !this.canReorderItems()) { this.teardownDragController(); return; } const reorderableCount = this.getReorderableOrder().length; if (reorderableCount < 2) { this.teardownDragController(); return; } if (this.sortable) { this.sortable.option('handle', DRAG_HANDLE_SELECTOR); this.sortable.option('draggable', DRAGGABLE_ITEM_SELECTOR); this.sortable.option('disabled', false); this.sortable.option('delay', TOUCH_DRAG_DELAY_MS); this.sortable.option('delayOnTouchOnly', true); return; } this.sortable = Sortable.create(this.container, { animation: 150, handle: DRAG_HANDLE_SELECTOR, draggable: DRAGGABLE_ITEM_SELECTOR, delay: TOUCH_DRAG_DELAY_MS, delayOnTouchOnly: true, onStart: this.handleSortStart, onEnd: this.handleSortEnd, }); } teardownDragController() { if (this.sortable) { this.sortable.destroy(); this.sortable = undefined; } this.clearDropElevationTimer(); if (this.dropElevationTarget) { this.dropElevationTarget.classList.remove(DEFAULT_DROP_ELEVATION_CLASS); this.dropElevationTarget = undefined; } if (this.container) { this.container.classList.remove(DEFAULT_CONTAINER_CLASS); } this.dragSnapshot = undefined; this.draggedItemIndex = undefined; } canReorderItems() { if (this.props.disabled || this.props.readonly) { return false; } const schema = this.props.schema; const limeOptions = (schema === null || schema === void 0 ? void 0 : schema.lime) || {}; return limeOptions.allowItemReorder !== false; } canRemoveItems() { const schema = this.props.schema; const limeOptions = (schema === null || schema === void 0 ? void 0 : schema.lime) || {}; return limeOptions.allowItemRemoval !== false; } isItemReorderable(item) { return (this.canReorderItems() && Boolean((item === null || item === void 0 ? void 0 : item.hasMoveDown) || (item === null || item === void 0 ? void 0 : item.hasMoveUp))); } isIndexReorderable(index) { const item = this.itemByIndex.get(index); if (!item) { return false; } return this.isItemReorderable(item); } getReorderableOrder(order = this.state.order) { const result = []; for (const index of order) { if (this.isIndexReorderable(index)) { result.push(index); } } return result; } readOrderFromDom() { if (!this.container) { return []; } // Only read the order of the direct children of this array field. // Nested array fields may render `.array-item` elements inside items, // and those must not affect the parent order. const items = [...this.container.children].filter((element) => { var _a, _b; return Boolean((_b = (_a = element === null || element === void 0 ? void 0 : element.classList) === null || _a === void 0 ? void 0 : _a.contains) === null || _b === void 0 ? void 0 : _b.call(_a, 'array-item')); }); const order = []; for (const element of items) { const index = this.getReorderId(element); if (index !== undefined) { order.push(index); } } return order; } getReorderId(element) { var _a; if (!element) { return undefined; } const value = (_a = element === null || element === void 0 ? void 0 : element.dataset) === null || _a === void 0 ? void 0 : _a.reorderId; if (value === undefined) { return undefined; } const parsed = Number.parseInt(value, 10); return Number.isNaN(parsed) ? undefined : parsed; } applyDropElevation(item) { if (this.dropElevationTarget && this.dropElevationTarget !== item) { this.dropElevationTarget.classList.remove(DEFAULT_DROP_ELEVATION_CLASS); } this.clearDropElevationTimer(); item.classList.add(DEFAULT_DROP_ELEVATION_CLASS); this.dropElevationTarget = item; } scheduleDropElevationRemoval() { if (!this.dropElevationTarget) { return; } const target = this.dropElevationTarget; this.clearDropElevationTimer(); this.dropElevationTimeout = globalThis.setTimeout(() => { target.classList.remove(DEFAULT_DROP_ELEVATION_CLASS); if (this.dropElevationTarget === target) { this.dropElevationTarget = undefined; } this.dropElevationTimeout = undefined; }, DROP_ELEVATION_DURATION); } clearDropElevationTimer() { if (this.dropElevationTimeout !== undefined) { clearTimeout(this.dropElevationTimeout); this.dropElevationTimeout = undefined; } } arraysEqual(a, b) { if (a.length !== b.length) { return false; } let index = 0; for (const value of a) { if (value !== b[index]) { return false; } index += 1; } return true; } extractIndices(items = []) { return (items !== null && items !== void 0 ? items : []).map((item, index) => { var _a; return (_a = item.index) !== null && _a !== void 0 ? _a : index; }); } }