/* eslint-disable no-use-before-define */ /** * The DragManager handles all dragging interactions that occur entirely within Dash (as opposed to external drag operations from the file system, etc) * * Events are generated for * a pause in the drag movement (dashDragMovePause) as a Doc(s) is dragged, * just before (dashPreDrop) a Doc(s) is dropped, * and just after (dashDropEvent) a Doc(s) is dropped * If the document is dragged and paused over the golden layout header tabs, the * drag interaction will switch to a golden layout tab drag. * * All drag operations can be aborted by hitting the Esc key * */ import { action, observable, runInAction } from 'mobx'; import { ClientUtils } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { DateField } from '../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, FieldType, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { ScriptField } from '../../fields/ScriptField'; import { ScriptCast, StrCast } from '../../fields/Types'; import { Docs } from '../documents/Documents'; import { DocumentView } from '../views/nodes/DocumentView'; import { dropActionType } from './DropActionTypes'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore /** * Initialize drag * @param _reference: The HTMLElement that is being dragged * @param docFunc: The Dash document being moved */ export function SetupDrag(_reference: React.RefObject, docFunc: () => Doc | undefined) { const onRowMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); const doc = docFunc(); if (doc) { const dragData = new DragManager.DocumentDragData([doc]); DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); } }; const onRowUp = (): void => { document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); }; const onItemDown = (e: React.PointerEvent) => { if (e.button === 0) { e.stopPropagation(); if (e.shiftKey) { e.persist(); const dragDoc = docFunc(); dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]); } else { document.addEventListener('pointermove', onRowMove); document.addEventListener('pointerup', onRowUp); } } }; return onItemDown; } export namespace DragManager { export const dragClassName = 'collectionFreeFormDocumentView-container'; let dragDiv: HTMLDivElement; let dragLabel: HTMLDivElement; export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => boolean>; export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; export let AbortDrag: () => void = emptyFunction; export const docsBeingDragged: Doc[] = observable([]); export let DraggedDocs: Doc[] | undefined; export function Root() { const root = document.getElementById('root'); if (!root) { throw new Error('No root element found'); } return root; } export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; export type RemoveFunction = (document: Doc | Doc[]) => boolean; export interface DragDropDisposer { (): void; } export interface DragOptions { dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed hideSource?: boolean; // hide source document during drag noAutoscroll?: boolean; } // event called when the drag operation results in a drop action export class DropEvent { // eslint-disable-next-line no-useless-constructor constructor( readonly x: number, readonly y: number, readonly complete: DragCompleteEvent, readonly shiftKey: boolean, readonly altKey: boolean, readonly metaKey: boolean, readonly ctrlKey: boolean, readonly embedKey: boolean ) { /* empty */ } } // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated export class DragCompleteEvent { constructor(aborted: boolean, dragData: DocumentDragData | AnchorAnnoDragData | LinkDragData | ColumnDragData) { this.aborted = aborted; this.docDragData = dragData instanceof DocumentDragData ? dragData : undefined; this.annoDragData = dragData instanceof AnchorAnnoDragData ? dragData : undefined; this.linkDragData = dragData instanceof LinkDragData ? dragData : undefined; this.columnDragData = dragData instanceof ColumnDragData ? dragData : undefined; } linkDocument?: Doc; aborted: boolean; docDragData?: DocumentDragData; annoDragData?: AnchorAnnoDragData; linkDragData?: LinkDragData; columnDragData?: ColumnDragData; } export class DocumentDragData { constructor(dragDoc: Doc[], dropAction?: dropActionType) { this.draggedDocuments = dragDoc; this.droppedDocuments = []; this.offset = [0, 0]; this.dropAction = dropAction; } dragEnding?: () => void; dragStarting?: () => void; draggedDocuments: Doc[]; droppedDocuments: Doc[]; treeViewDoc?: Doc; offset: number[]; canEmbed?: boolean; userDropAction?: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action dropAction?: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move' dropPropertiesToRemove?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts } Doc.SetDocDragDataName(DocumentDragData.name); export class LinkDragData { constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc) { this.linkDragView = dragView; this.linkSourceGetAnchor = linkSourceGetAnchor; } get dragDocument() { return this.linkDragView.Document; } linkSourceGetAnchor: () => Doc; linkSourceDoc?: Doc; linkDragView: DocumentView; get canEmbed() { return true; } } export class ColumnDragData { // constructor(colKey: SchemaHeaderField) { // this.colKey = colKey; // } // colKey: SchemaHeaderField; constructor(colIndex: number) { this.colIndex = colIndex; } colIndex: number; get canEmbed() { return true; } } // used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made. // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag export class AnchorAnnoDragData extends LinkDragData { constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc, dropDocCreator: (annotationOn: Doc | undefined) => Doc) { super(dragView, linkSourceGetAnchor); this.dropDocCreator = dropDocCreator; this.offset = [0, 0]; } dropDocCreator: (annotationOn: Doc | undefined) => Doc; dropDocument?: Doc; offset: number[]; dropAction?: dropActionType; userDropAction?: dropActionType; get canEmbed() { return true; } } const defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { if (de.complete.docDragData) { targetAction && (de.complete.docDragData.dropAction = targetAction); e.stopPropagation(); } }; export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer { if ('canDrop' in element.dataset) { throw new Error("Element is already droppable, can't make it droppable again"); } element.dataset.canDrop = 'true'; const handler = (e: Event) => dropFunc(e, (e as CustomEvent).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent).detail; (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc.dropAction) as dropActionType); }; element.addEventListener('dashOnDrop', handler); element.addEventListener('dashPreDrop', preDropHandler); return () => { element.removeEventListener('dashOnDrop', handler); element.removeEventListener('dashPreDrop', preDropHandler); delete element.dataset.canDrop; }; } // drag a document and drop it (or make an embed/copy on drop) export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => unknown) { const addAudioTag = (dropDoc: Doc) => { dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField()); dropDoc instanceof Doc && CreateLinkToActiveAudio(() => dropDoc); return dropDoc; }; const finishDrag = async (e: DragCompleteEvent) => { const { docDragData } = e; setTimeout(() => dragData.dragEnding?.()); onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = ( await Promise.all( dragData.draggedDocuments.map(async d => !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result as Doc) : docDragData.dropAction === dropActionType.embed ? Doc.BestEmbedding(d) : docDragData.dropAction === dropActionType.add ? d : docDragData.dropAction === dropActionType.proto ? d[DocData] : docDragData.dropAction === dropActionType.copy ? (await Doc.MakeClone(d)).clone : d ) ) ).filter(d => d); ![dropActionType.same, dropActionType.proto].includes(StrCast(docDragData.dropAction) as dropActionType) && docDragData.droppedDocuments // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any)) .forEach((drop: Doc, i: number) => { const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove); const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps)); [...remProps, 'dropPropertiesToRemove'].forEach(prop => { drop[prop] = undefined; }); }); } return e; }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); dragData.dragStarting?.(); return true; } // drag a button template and drop a new button export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: FieldType }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { const finishDrag = (e: DragCompleteEvent) => { const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }); params.forEach(p => { Object.keys(vars).indexOf(p) !== -1 && (bd[DocData][p] = new PrefetchProxy(vars[p] as Doc)); }); // copy all "captured" arguments into document parameterfields initialize?.(bd); bd[DocData]['onClick-paramFieldKeys'] = new List(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); return e; }; // eslint-disable-next-line no-param-reassign options = options ?? {}; options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption export function StartAnchorAnnoDrag(eles: HTMLElement[], dragData: AnchorAnnoDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(eles, dragData, downX, downY, options); } // drags a linker button and creates a link on drop export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | ((addAsAnnotation: boolean) => Doc), downX: number, downY: number, options?: DragOptions) { StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.Document), downX, downY, options); } // drags a column from a schema view export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column'); } export function snapDragAspect(dragPt: number[], snapAspect: number) { let closest = ClientUtils.SNAP_THRESHOLD; let near = dragPt; const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => { if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0 const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (denominator === 0) return undefined; // Lines are parallel const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; // let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; // if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return undefined; // is the intersection along the segments // Return a object with the x and y coordinates of the intersection const x = x1 + ua * (x2 - x1); const y = y1 + ua * (y2 - y1); const dist = Math.sqrt((dragx - x) * (dragx - x) + (dragy - y) * (dragy - y)); return { pt: [x, y], dist }; }; SnappingManager.VertSnapLines.forEach(xCoord => { const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, xCoord, -1, xCoord, 1, dragPt[0], dragPt[1]); if (pt && pt.dist < closest) { closest = pt.dist; near = pt.pt; } }); SnappingManager.HorizSnapLines.forEach(yCoord => { const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, -1, yCoord, 1, yCoord, dragPt[0], dragPt[1]); if (pt && pt.dist < closest) { closest = pt.dist; near = pt.pt; } }); return { x: near[0], y: near[1] }; } // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { const snapThreshold = ClientUtils.SNAP_THRESHOLD; const snapVal = (pts: number[], drag: number, snapLines: number[]) => { if (snapLines.length) { const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt const rangePts = [drag - offs[0], drag - offs[1], drag - offs[2]]; // left, mid, right or top, mid, bottom pts to try to snap to snaplines const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => (Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest))); const closestDists = rangePts.map((pt, i) => Math.abs(pt - closestPts[i])); const minIndex = closestDists[0] < closestDists[1] && closestDists[0] < closestDists[2] ? 0 : closestDists[1] < closestDists[2] ? 1 : 2; return closestDists[minIndex] < snapThreshold ? closestPts[minIndex] + offs[minIndex] : drag; } return drag; }; return { x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.VertSnapLines), y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.HorizSnapLines), }; } async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { const dropArgs = { cancelable: true, // allows preventDefault() to be called to cancel the drop bubbles: true, detail: { ...pos, complete, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, embedKey: SnappingManager.CanEmbed, }, }; target.dispatchEvent(new CustomEvent('dashPreDrop', dropArgs)); UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes) await finishDrag?.(complete); UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called options?.dragComplete?.(complete); endDrag?.(); } export function StartDrag( elesIn: HTMLElement[], dragData: DocumentDragData | LinkDragData | ColumnDragData | AnchorAnnoDragData, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string ) { if (SnappingManager.ExploreMode) return; const docDragData = dragData instanceof DocumentDragData ? dragData : undefined; DraggedDocs = docDragData?.draggedDocuments; const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); const eles = elesIn.filter(e => e); SnappingManager.SetCanEmbed(dragData.canEmbed || false); if (!dragDiv) { dragDiv = document.createElement('div'); dragDiv.className = 'dragManager-dragDiv'; dragDiv.style.pointerEvents = 'none'; dragLabel = document.createElement('div'); dragLabel.className = 'dragManager-dragLabel'; dragLabel.style.zIndex = '100001'; dragLabel.style.fontSize = '10px'; dragLabel.style.position = 'absolute'; dragLabel.style.background = '#ffffff90'; dragLabel.innerText = 'drag titlebar to embed on drop'; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } Object.assign(dragDiv.style, { width: '', height: '', overflow: '' }); dragDiv.hidden = false; const scalings: number[] = []; const xs: number[] = []; const ys: number[] = []; const elesCont = { left: Number.MAX_SAFE_INTEGER, right: Number.MIN_SAFE_INTEGER, top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER, }; const rot: number[] = []; const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element // bcz: used to be false, but that made dragging collection w/ native dim's not work... let useDim = true; // if doc is rotated by freeformview, then the dragged elements width and height won't reflect the unrotated dimensions, so we need to rely on the element knowing its own width/height. \ // if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu) let rotation: number | undefined; for (let parEle: HTMLElement | null | undefined = ele.parentElement; parEle; parEle = parEle?.parentElement) { if (parEle.className === DragManager.dragClassName) { rotation = (rotation ?? 0) + Number(parEle.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0); } parEle = parEle.parentElement; } if (rotation !== undefined) { rot.push(rotation); } else { useDim = true; rot.push(0); } if (!ele.parentNode) dragDiv.appendChild(ele); const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement); const children = Array.from(dragElement.children); while (children.length) { // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag const next = children.pop(); next && children.push(...Array.from(next.children)); if (next) { ['marker-start', 'marker-mid', 'marker-end'].forEach(field => { if (next.localName.startsWith('path')) { const item = next.attributes.getNamedItem(field); item && next.setAttribute(field, item.value.replace('#', '#X')); } }); if (next.localName.startsWith('marker')) { next.id = 'X' + next.id; } } } const rect = ele.getBoundingClientRect(); const w = ele.offsetWidth || rect.width; const h = ele.offsetHeight || rect.height; const rotR = -((rot.lastElement() < 0 ? rot.lastElement() + 360 : rot.lastElement()) / 180) * Math.PI; const tl = [0, 0]; const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w]; const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h]; const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w]; const minx = Math.min(tl[0], tr[0], br[0], bl[0]); const maxx = Math.max(tl[0], tr[0], br[0], bl[0]); const miny = Math.min(tl[1], tr[1], br[1], bl[1]); const maxy = Math.max(tl[1], tr[1], br[1], bl[1]); const scaling = rect.width / (Math.abs(maxx - minx) || 1); elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); elesCont.bottom = Math.max(rect.bottom, elesCont.bottom); xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left); ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top); scalings.push(scaling); const width = useDim ? getComputedStyle(ele).width : ''; const height = useDim ? getComputedStyle(ele).height : ''; Object.assign(dragElement.style, { opacity: '0.7', position: 'absolute', margin: '0', top: '0', bottom: '', left: '0', color: 'black', transition: 'none', borderRadius: getComputedStyle(ele).borderRadius, zIndex: contextMenuZindex, transformOrigin: '0 0', width, height, transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot.lastElement()}deg) scale(${scaling})`, }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; if (docsToDrag.length) { const pdfBox = dragElement.getElementsByTagName('canvas'); const pdfBoxSrc = ele.getElementsByTagName('canvas'); Array.from(pdfBox) .filter(pb => pb.width && pb.height) .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))] .map(dele => (dele as HTMLElement)?.style) .forEach(style => { style && (style.pointerEvents = 'none'); }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)]; while (dragChildren[0].length) { const childs = [dragChildren[0].pop(), dragChildren[1].pop()]; if (childs[0]?.children) { dragChildren[0].push(...Array.from(childs[0].children)); dragChildren[1].push(...Array.from(childs[1]!.children)); } if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; } } return dragElement; }); runInAction(() => docsBeingDragged.push(...docsToDrag)); const hideDragShowOriginalElements = (hide: boolean) => { dragLabel.style.display = hide && !SnappingManager.CanEmbed ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); setTimeout(() => eles.forEach(ele => { ele.hidden = hide; }) ); }; options?.hideSource && hideDragShowOriginalElements(true); SnappingManager.SetIsDragging(true); let lastPt = { x: downX, y: downY }; const xFromLeft = downX - elesCont.left; const yFromTop = downY - elesCont.top; const xFromRight = elesCont.right - downX; const yFromBottom = elesCont.bottom - downY; let scrollAwaiter: Opt; let startWindowDragTimer: NodeJS.Timeout | undefined; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (docDragData) { docDragData.userDropAction = e.ctrlKey && e.altKey ? dropActionType.copy : e.shiftKey ? dropActionType.move : e.ctrlKey ? dropActionType.embed : docDragData.defaultDropAction; const targClassName = e.target instanceof HTMLElement && typeof e.target.className === 'string' ? e.target.className : ''; if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(targClassName) && docDragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; docDragData.dropAction = docDragData.userDropAction || dropActionType.same; AbortDrag(); await finishDrag?.(new DragCompleteEvent(true, docDragData)); DragManager.StartWindowDrag?.(e, docDragData.droppedDocuments, aborted => { if (!aborted && (docDragData?.dropAction === 'move' || docDragData?.dropAction === 'same')) { docDragData.removeDocument?.(docDragData?.draggedDocuments[0]); } }); }, 500); } } else { clearTimeout(startWindowDragTimer); startWindowDragTimer = undefined; } } const target = document.elementFromPoint(e.x, e.y); if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !(docDragData?.draggedDocuments as Doc[])?.some(d => d._freeform_noAutoPan)) { const autoScrollHandler = () => { target.dispatchEvent( new CustomEvent('dashDragMovePause', { bubbles: true, detail: { shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, clientX: e.clientX, clientY: e.clientY, dataTransfer: new DataTransfer(), button: e.button, buttons: e.buttons, getModifierState: e.getModifierState, movementX: e.movementX, movementY: e.movementY, pageX: e.pageX, pageY: e.pageY, relatedTarget: e.relatedTarget, screenX: e.screenX, screenY: e.screenY, detail: e.detail, view: { ...(e.view ?? new Window()), styleMedia: { type: '', matchMedium: () => false } }, // bcz: Ugh.. this looks wrong nativeEvent: new DragEvent('dashDragMovePause'), currentTarget: target, target: target, bubbles: true, cancelable: true, defaultPrevented: true, eventPhase: e.eventPhase, isTrusted: true, preventDefault: () => 'not implemented for this event', isDefaultPrevented: () => false, stopPropagation: () => 'not implemented for this event', isPropagationStopped: () => false, persist: emptyFunction, timeStamp: e.timeStamp, type: 'dashDragMovePause', }, }) ); scrollAwaiter && clearTimeout(scrollAwaiter); SnappingManager.IsDragging && (scrollAwaiter = setTimeout(autoScrollHandler, 25)); }; scrollAwaiter && clearTimeout(scrollAwaiter); scrollAwaiter = setTimeout(autoScrollHandler, 250); } const { x, y } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); const moveVec = { x: x - lastPt.x, y: y - lastPt.y }; lastPt = { x, y }; dragElements.forEach((dragElement, i) => { dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg) scale(${scalings[i]})`; }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; }; const upHandler = (e: PointerEvent) => { clearTimeout(startWindowDragTimer); startWindowDragTimer = undefined; dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false)); }; const cleanupDrag = action((undo: boolean) => { (dragData as DocumentDragData).dragEnding?.(); hideDragShowOriginalElements(false); document.removeEventListener('pointermove', moveHandler, true); document.removeEventListener('pointerup', upHandler, true); SnappingManager.SetIsDragging(false); if (batch.end() && undo) UndoManager.Undo(); docsBeingDragged.length = 0; SnappingManager.SetCanEmbed(false); }); AbortDrag = () => { options?.dragComplete?.(new DragCompleteEvent(true, dragData)); cleanupDrag(true); }; document.addEventListener('pointermove', moveHandler, true); document.addEventListener('pointerup', upHandler, true); } }