diff options
Diffstat (limited to 'src/client/util/DragManager.ts')
-rw-r--r-- | src/client/util/DragManager.ts | 198 |
1 files changed, 117 insertions, 81 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 35694a6bd..2a9c1633a 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,23 +1,17 @@ -import { Doc, Field, DocListCast } from "../../new_fields/Doc"; -import { Cast, ScriptCast, StrCast } from "../../new_fields/Types"; +import { action, observable, runInAction } from "mobx"; +import { DateField } from "../../fields/DateField"; +import { Doc, Field, Opt } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { PrefetchProxy } from "../../fields/Proxy"; +import { listSpec } from "../../fields/Schema"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { ScriptField } from "../../fields/ScriptField"; +import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; import { emptyFunction } from "../../Utils"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import * as globalCssVariables from "../views/globalCssVariables.scss"; -import { DocumentManager } from "./DocumentManager"; -import { LinkManager } from "./LinkManager"; -import { SelectionManager } from "./SelectionManager"; -import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; import { Docs, DocUtils } from "../documents/Documents"; -import { ScriptField } from "../../new_fields/ScriptField"; -import { List } from "../../new_fields/List"; -import { PrefetchProxy } from "../../new_fields/Proxy"; -import { listSpec } from "../../new_fields/Schema"; -import { Scripting } from "./Scripting"; -import { convertDropDataToButtons } from "./DropConverter"; -import { AudioBox } from "../views/nodes/AudioBox"; -import { DateField } from "../../new_fields/DateField"; -import { DocumentView } from "../views/nodes/DocumentView"; +import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; +import { SnappingManager } from "./SnappingManager"; export type dropActionType = "alias" | "copy" | "move" | undefined; // undefined = move export function SetupDrag( @@ -53,10 +47,10 @@ export function SetupDrag( const onItemDown = async (e: React.PointerEvent) => { if (e.button === 0) { e.stopPropagation(); - if (e.shiftKey && CollectionDockingView.Instance) { + if (e.shiftKey) { e.persist(); const dragDoc = await docFunc(); - dragDoc && CollectionDockingView.Instance.StartOtherDrag({ + dragDoc && DragManager.StartWindowDrag?.({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, @@ -73,6 +67,7 @@ export function SetupDrag( export namespace DragManager { let dragDiv: HTMLDivElement; + export let StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined; export function Root() { const root = document.getElementById("root"); @@ -82,8 +77,8 @@ export namespace DragManager { return root; } export let AbortDrag: () => void = emptyFunction; - export type MoveFunction = (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; - export type RemoveFunction = (document: Doc) => boolean; + 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 { @@ -180,7 +175,8 @@ export namespace DragManager { export function MakeDropTarget( element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, - doc?: Doc + doc?: Doc, + preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void, ): DragDropDisposer { if ("canDrop" in element.dataset) { throw new Error( @@ -191,9 +187,7 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - if (de.complete.docDragData && doc?.targetDropAction) { - de.complete.docDragData.dropAction = StrCast(doc.targetDropAction) as dropActionType; - } + preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType); }; element.addEventListener("dashOnDrop", handler); doc && element.addEventListener("dashPreDrop", preDropHandler); @@ -208,7 +202,7 @@ export namespace DragManager { export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); - dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline")); + dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(dropDoc); return dropDoc; }; const batch = UndoManager.StartBatch("dragging"); @@ -239,40 +233,6 @@ export namespace DragManager { StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } - // drag links and drop link targets (aliasing them if needed) - export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { - const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; - - if (draggedDocs.length) { - const moddrag: Doc[] = []; - for (const draggedDoc of draggedDocs) { - const doc = await Cast(draggedDoc.annotationOn, Doc); - if (doc) moddrag.push(doc); - } - - const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); - dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => { - docView.props.removeDocument?.(doc); - addDocument(doc); - return true; - }; - const containingView = docView.props.ContainingCollectionView; - const finishDrag = (e: DragCompleteEvent) => - e.docDragData && (e.docDragData.droppedDocuments = - dragData.draggedDocuments.reduce((droppedDocs, d) => { - const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView); - if (dvs.length) { - dvs.forEach(dv => droppedDocs.push(dv.props.Document)); - } else { - droppedDocs.push(Doc.MakeAlias(d)); - } - return droppedDocs; - }, [] as Doc[])); - - StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag); - } - } - // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption export function StartPdfAnnoDrag(eles: HTMLElement[], dragData: PdfAnnoDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(eles, dragData, downX, downY, options); @@ -292,7 +252,64 @@ export namespace DragManager { StartDrag([ele], {}, downX, downY); } - function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { + export function SetSnapLines(horizLines: number[], vertLines: number[]) { + SnappingManager.setSnapLines(horizLines, vertLines); + } + export function snapDragAspect(dragPt: number[], snapAspect: number) { + let closest = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); + 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, i) => { + 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, i) => { + 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 { thisX: near[0], thisY: 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 = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); + 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 { + thisX: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), + thisY: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()) + }; + } + export let docsBeingDragged: Doc[] = []; + export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { eles = eles.filter(e => e); if (!dragDiv) { dragDiv = document.createElement("div"); @@ -300,19 +317,29 @@ export namespace DragManager { dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } - SelectionManager.SetIsDragging(true); + SnappingManager.SetIsDragging(true); const scaleXs: number[] = []; const scaleYs: number[] = []; const xs: number[] = []; const ys: number[] = []; - const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : []; + docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : []; + const elesCont = { + left: Number.MAX_SAFE_INTEGER, + top: Number.MAX_SAFE_INTEGER, + right: Number.MIN_SAFE_INTEGER, + bottom: Number.MIN_SAFE_INTEGER + }; const dragElements = eles.map(ele => { if (!ele.parentNode) dragDiv.appendChild(ele); const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; const rect = ele.getBoundingClientRect(); const scaleX = rect.width / ele.offsetWidth, scaleY = rect.height / ele.offsetHeight; + 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(rect.left); ys.push(rect.top); scaleXs.push(scaleX); @@ -332,7 +359,7 @@ export namespace DragManager { dragElement.style.width = `${rect.width / scaleX}px`; dragElement.style.height = `${rect.height / scaleY}px`; - if (docs.length) { + if (docsBeingDragged.length) { const pdfBox = dragElement.getElementsByTagName("canvas"); const pdfBoxSrc = ele.getElementsByTagName("canvas"); Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); @@ -362,31 +389,38 @@ export namespace DragManager { let lastX = downX; let lastY = downY; + const xFromLeft = downX - elesCont.left; + const yFromTop = downY - elesCont.top; + const xFromRight = elesCont.right - downX; + const yFromBottom = elesCont.bottom - downY; let alias = "alias"; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined; } - if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) { + if (e.shiftKey && dragData.droppedDocuments.length === 1) { !dragData.dropAction && (dragData.dropAction = alias); if (dragData.dropAction === "move") { dragData.removeDocument?.(dragData.draggedDocuments[0]); } AbortDrag(); finishDrag?.(new DragCompleteEvent(true, dragData)); - CollectionDockingView.Instance.StartOtherDrag({ + DragManager.StartWindowDrag?.({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }, dragData.droppedDocuments); } + + const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); + alias = "move"; - const moveX = e.pageX - lastX; - const moveY = e.pageY - lastY; - lastX = e.pageX; - lastY = e.pageY; + const moveX = thisX - lastX; + const moveY = thisY - lastY; + lastX = thisX; + lastY = thisY; dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) ); @@ -396,21 +430,22 @@ export namespace DragManager { dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false)); }; - const endDrag = () => { + const endDrag = action(() => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - }; + SnappingManager.clearSnapLines(); + }); AbortDrag = () => { hideDragShowOriginalElements(); - SelectionManager.SetIsDragging(false); + SnappingManager.SetIsDragging(false); options?.dragComplete?.(new DragCompleteEvent(true, dragData)); endDrag(); }; const upHandler = (e: PointerEvent) => { hideDragShowOriginalElements(); - dispatchDrag(eles, e, dragData, options, finishDrag); - SelectionManager.SetIsDragging(false); + dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); + SnappingManager.SetIsDragging(false); endDrag(); options?.dragComplete?.(new DragCompleteEvent(false, dragData)); }; @@ -418,7 +453,8 @@ export namespace DragManager { document.addEventListener("pointerup", upHandler); } - function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { + function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, + xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => { const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height, o: dragEle.style.overflow }; dragEle.style.width = "0"; @@ -432,14 +468,15 @@ export namespace DragManager { r.ele.style.height = r.h; r.ele.style.overflow = r.o; }); + const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); if (target) { const complete = new DragCompleteEvent(false, dragData); target.dispatchEvent( new CustomEvent<DropEvent>("dashPreDrop", { bubbles: true, detail: { - x: e.x, - y: e.y, + x: thisX, + y: thisY, complete: complete, shiftKey: e.shiftKey, altKey: e.altKey, @@ -453,8 +490,8 @@ export namespace DragManager { new CustomEvent<DropEvent>("dashOnDrop", { bubbles: true, detail: { - x: e.x, - y: e.y, + x: thisX, + y: thisY, complete: complete, shiftKey: e.shiftKey, altKey: e.altKey, @@ -466,4 +503,3 @@ export namespace DragManager { } } } -Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }); |