import { NestedTableKeyedItem, NestedTableNestedModeState } from '../../state'; import { NestedTableNodeState } from '../../state/NestedTableState/NestedTableNodeState'; import { createOptimisticActionsOrderByModeReaction } from '../../state/DragAndDrop/createOptimisticActionsOrderByModeReaction'; import { DragAndDropState, IndexedItem } from '../DragAndDrop'; import { action, computed, makeObservable } from 'mobx'; import { ActionMove, CollectionState, ComputedQuery, FiltersMap, TypedEmitter, } from '@wix/bex-core'; import { EventEmitter } from 'events'; export interface NestedTableDragAndDropStateParams { readonly nestedState: NestedTableNestedModeState; } export interface NestedDragEndEvent { from: IndexedItem; after: IndexedItem<{}> | null; over: IndexedItem<{}>; parentId: string | undefined; filters: ComputedQuery['filters']; node: NestedTableNodeState; } export interface DragAndDropDisabledParams { from: IndexedItem; over: IndexedItem<{}>; } export class NestedTableDragAndDropState { readonly tableState; readonly dnd; readonly nestedState; _isExpandedBeforeDrag?: boolean; constructor(params: NestedTableDragAndDropStateParams) { const { nestedState } = params; const { container } = nestedState; this.tableState = nestedState.tableState; this.nestedState = nestedState; this.dnd = new DragAndDropState({ container, state: nestedState.wrapper.toolbar, collection: nestedState, move: () => this._move(), announcements: { dragHandleLabel: (id) => { const { translate: t } = container; const item = nestedState.getKeyedItem(id as string)?.item; const itemName = item?.state.collection.itemName; return t('cairo.dragAndDrop.button-role-label.sr', { itemName: item && itemName ? itemName(item.originalKeyedItem?.item) : '', }); }, itemDescription: (id) => { const item = nestedState.getKeyedItem(id as string)?.item; const itemName = item?.state.collection.itemName; return item && itemName ? itemName(item.originalKeyedItem?.item) : ''; }, }, }); this.dnd.events.on('dragStart', () => { const { activeNode } = this; this._isExpandedBeforeDrag = activeNode?.node._expanded; if (activeNode?.node._expanded) { activeNode.node._toggleExpand(); } }); this.dnd.onCancel = this._onCancel; makeObservable(this, { dragEventItems: computed, dragEventDirectionItems: computed, dragEventDepth: computed, activeNode: computed, dragEventParentAndAfter: computed, reachedMaxDepth: computed, dragAndDropDisabled: computed, _createDragEndEvent: action, _move: action, }); } _onCancel = async () => { const { dragEventParentAndAfter, dragAndDropDisabled } = this; const { node, activeItem, overItem } = dragEventParentAndAfter; if (dragAndDropDisabled) { return true; } if ( !node || !activeItem || !overItem || !activeItem.item.originalKeyedItem ) { return null; } const dragEndEvent = this._createDragEndEvent(); if (!dragEndEvent) { return null; } const { levelDescriptor } = activeItem.item.state; const shouldCancel = await levelDescriptor.dragAndDropCancel?.( this._createSimpleDragEndEvent(dragEndEvent, node), ); if (!shouldCancel) { return false; } this.dnd._maybeShowErrorToast(shouldCancel, dragEndEvent); return true; }; private _createNewParentData( node: NestedTableNodeState, ) { if (!node.parent || !node.parentNode) { return {}; } return { parentItem: node.parent.item as {}, }; } _createDragEndEvent({ setNewParent = true, }: { setNewParent?: boolean } = {}): ActionMove | null { const { dragEventParentAndAfter, dnd, nestedState } = this; const { node, activeItem, after, overItem } = dragEventParentAndAfter; if ( !node || !activeItem || !overItem || !activeItem.item.originalKeyedItem || !overItem.item.originalKeyedItem ) { return null; } const { levelDescriptor } = activeItem.item.state; if (!levelDescriptor.setParent) { if (process.env.NODE_ENV !== 'production') { console.warn( 'setParent is not defined in levelDescriptor, skipping drag end event creation', ); } return null; } return { moveId: dnd._moveId, from: setNewParent ? { ...activeItem.item.originalKeyedItem, item: levelDescriptor.setParent( activeItem.item.originalKeyedItem.item, node.parent?.id, this._createNewParentData(node), ), } : activeItem.item.originalKeyedItem, over: overItem.item.originalKeyedItem, after: after?.item.originalKeyedItem, query: nestedState.query.asComputed, filtersKeyHash: nestedState.query.filtersKeyHash, isFromHandle: Boolean(dnd._isHandleActive), }; } _createSimpleDragEndEvent( dragEndEvent: ActionMove, node: NestedTableNodeState, ) { const { dnd } = this; return { ...dnd._createSimpleDragEndEvent(dragEndEvent), parentId: node.parent?.id, node, }; } _move() { const { dragEventParentAndAfter, nestedState, reachedMaxDepth, activeNode, } = this; if (reachedMaxDepth) { if ( activeNode && this._isExpandedBeforeDrag != null && this._isExpandedBeforeDrag !== activeNode.node._expanded ) { activeNode._toggleExpand(); } return; } const { node, activeItem, overItem } = dragEventParentAndAfter; if ( !node || !activeItem || !overItem || !activeItem.item.originalKeyedItem ) { return; } const dragEndEvent = this._createDragEndEvent(); if (!dragEndEvent) { return; } /** * A submit function that will be shared between 3 optimistic actions. * How it works: The `dragAndDropSubmit` function provided by the consumer will be called only inside the `move` action submit function. * Using EventEmitter to resolve the promise only after the `move` action submit function completes. */ const submitEvents = new EventEmitter() as TypedEmitter<{ submit: (err?: unknown) => void; }>; const submit = () => new Promise((resolve, reject) => submitEvents.once('submit', (err) => (err ? reject(err) : resolve())), ); activeItem.item.state.collection.optimisticActions.updateOne( dragEndEvent.from.item, { submit, keepPosition: true, }, ); if ( activeItem.item.state === node || node.unprocessedMap.has(dragEndEvent.from.key) ) { node.collection.optimisticActions.updateOne(dragEndEvent.from.item, { submit, keepPosition: true, }); } else { node.collection.optimisticActions.createOne(dragEndEvent.from.item, { submit, }); } node.collection.optimisticActions.move(dragEndEvent, { submit: async () => { const { levelDescriptor } = activeItem.item.state; try { await levelDescriptor.dragAndDropSubmit?.( this._createSimpleDragEndEvent(dragEndEvent, node), ); submitEvents.emit('submit'); } catch (e) { submitEvents.emit('submit', e); throw e; } }, collectionSnapshot: nestedState.getCollectionSnapshot(), }); const nodeAfterMove = node.createNode({ parent: activeItem.item.originalKeyedItem, notifyAll: true, }); if (nodeAfterMove && this._isExpandedBeforeDrag != null) { nodeAfterMove.node._expanded = this._isExpandedBeforeDrag; } node._notify(); node.parentNode?._notify(); node._preRegisterAll(); } getStickyColumnsCount({ stickyColumns, horizontalScroll, }: { stickyColumns: number; showSelection?: boolean; horizontalScroll?: boolean; }) { const { dnd } = this; return horizontalScroll && !dnd.isDisabled ? stickyColumns + 1 : stickyColumns; } get dragEventItems() { const { dnd } = this; const { collection, activeId, overId } = dnd; if (!overId || !activeId) { return null; } const overItem = collection.getKeyedItem(overId) as NestedTableKeyedItem< C, any, any >; const activeItem = collection.getKeyedItem( activeId, ) as NestedTableKeyedItem; if (activeItem == null || overItem == null) { return null; } return { overItem, activeItem, }; } get dragEventDirectionItems() { const { nestedState: { keyedItems: items }, dragEventItems, } = this; if (!dragEventItems) { return null; } const { overItem, activeItem } = dragEventItems; const activeItemIndex = activeItem.index; const overItemIndex = overItem.index; const previousItem: NestedTableKeyedItem | null = activeItemIndex >= overItemIndex ? items[overItemIndex - 1] : items[overItemIndex]; const nextItem: NestedTableKeyedItem | null = activeItemIndex <= overItemIndex ? items[overItemIndex + 1] : items[overItemIndex]; return { nextItem, previousItem, overItem, activeItem, }; } get dragEventDepth() { const { nestedState: { _indentationWidth }, dragEventDirectionItems, dnd: { _moveDelta }, } = this; if (!dragEventDirectionItems) { return null; } const { nextItem, previousItem, activeItem, overItem } = dragEventDirectionItems; const dragDepth = Math.round(_moveDelta.x / _indentationWidth); // getDragDepth const projectedDepth = activeItem.item.state.childrenDepth + dragDepth; const maxDepth = previousItem ? previousItem.item.state.childrenDepth + 1 : 0; const minDepth = nextItem ? nextItem.item.state.childrenDepth : 0; let depth = projectedDepth; if (!this.isSameEntityAsOtherLevels) { depth = activeItem.item.state.childrenDepth; } else if (projectedDepth >= maxDepth) { depth = maxDepth; } else if (projectedDepth < minDepth) { depth = minDepth; } return { depth, nextItem, previousItem, activeItem, overItem, }; } get activeNode() { const { dnd: { activeId }, nestedState, } = this; if (!activeId) { return null; } return nestedState.root._getNode(activeId); } get dragEventParentAndAfter() { const { nestedState, dragEventDepth } = this; if (!dragEventDepth) { return {}; } const { keyedItems: items } = nestedState; const { previousItem, depth, overItem, activeItem } = dragEventDepth; if (previousItem == null) { return { node: nestedState.root, after: null, // before: nestedState.root.keyedItems[0], activeItem, overItem, }; } if (depth === previousItem.item.state.childrenDepth) { return { node: previousItem.item.state, after: previousItem, // before: nextItem && nextItem.item.state.depth === depth ? nextItem : null, activeItem, overItem, }; } if (depth > previousItem.item.state.childrenDepth) { const node = previousItem.item.state.parentKeyToChildrenMap.get( previousItem.id, ); return { node, after: null, // before: node?.keyedItems[0], activeItem, overItem, }; } for (let i = previousItem.index; i >= 0; i--) { const item = items[i]; if (item.item.state.childrenDepth === depth) { return { node: item.item.state, after: item, activeItem, overItem, }; } } return { activeItem, overItem, }; } get reachedMaxDepth() { const { dragEventParentAndAfter, dragEventDirectionItems, nestedState, activeNode, } = this; if (!dragEventParentAndAfter || !dragEventDirectionItems) { return false; } const { node } = dragEventParentAndAfter; if (!node) { return true; } const activeNodeDeepestBranchDepth = activeNode?.deepestBranchDepth ?? 0; return ( node.levelDescriptor.depth + activeNodeDeepestBranchDepth >= nestedState.levels.length ); } get dragAndDropDisabled() { const { dragEventParentAndAfter } = this; const { node } = dragEventParentAndAfter; if (!node) { return null; } const dragEndEvent = this._createDragEndEvent({ setNewParent: false }); if (!dragEndEvent) { return null; } return node.levelDescriptor.dragAndDropDisabled?.( this._createSimpleDragEndEvent(dragEndEvent, node), ); } get isSameEntityAsOtherLevels() { return this.nestedState.levels[0]?.isSameEntityAsOtherLevels; } initCollection(collection: CollectionState) { return createOptimisticActionsOrderByModeReaction(this.dnd, collection); } init() { return this.dnd.init(); } }