@limetech/lime-elements
Version:
354 lines (353 loc) • 13.5 kB
JavaScript
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; });
}
}