import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { createSchema, makeInterface, listSpec } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; import { LinkManager } from "../../../util/LinkManager"; import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; import { DocumentViewProps, DocAfterFocusFunc } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import { PresBox } from "../../nodes/PresBox"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionViewType } from "../CollectionView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { isUndefined } from "lodash"; export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", _currentTimecode: "number", displayTimecode: "number", _currentFrame: "number", arrangeInit: ScriptField, _useClusters: "boolean", fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _viewTransition: "string", scrollHeight: "number", fitX: "number", fitY: "number", fitW: "number", fitH: "number" }); type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof collectionSchema, typeof documentSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentSchema, pageSchema); export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; parentActive: (outsideReaction: boolean) => boolean; scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) }; @observer export class CollectionFreeFormView extends CollectionSubView>(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; private _downY: number = 0; private _inkToTextStartX: number | undefined; private _inkToTextStartY: number | undefined; private _wordPalette: Map = new Map(); private _clusterDistance: number = 75; private _hitCluster: number = -1; private _layoutComputeReaction: IReactionDisposer | undefined; private _boundsReaction: IReactionDisposer | undefined; private _layoutPoolData = new ObservableMap(); private _layoutSizeData = new ObservableMap(); private _cachedPool: Map = new Map(); @observable private _pullCoords: number[] = [0, 0]; @observable private _pullDirection: string = ""; public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef(); @observable _marqueeRef = React.createRef(); @observable canPanX: boolean = true; @observable canPanY: boolean = true; @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); } @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; private zoomScaling = () => { const mult = this.fitToContentScaling / this.parentScaling; if (this.fitToContent) { const zs = !this.childDocs.length ? 1 : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); return mult * zs; } return mult * NumCast(this.Document[this.scaleFieldKey], 1); } @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); } @computed get cachedGetContainerTransform(): Transform { return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); } @computed get cachedGetTransform(): Transform { return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } private centeringShiftX = () => this.cachedCenteringShiftX; private centeringShiftY = () => this.cachedCenteringShiftY; private getTransform = () => this.cachedGetTransform.copy(); private getLocalTransform = () => this.cachedGetLocalTransform.copy(); private getContainerTransform = () => this.cachedGetContainerTransform.copy(); private getTransformOverlay = () => this.getContainerTransform().translate(1, 1); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); } addDocument = action((newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { retVal = this.props.addDocument(newBox); retVal && this.bringToFront(newBox); retVal && this.updateCluster(newBox); } else { retVal = this.props.addDocument(newBox); // bcz: deal with clusters } if (retVal) { const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox; for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { const x = newBox.x; const y = newBox.y; const w = newBox._width; const h = newBox._height; delete newBox["x-indexed"]; delete newBox["y-indexed"]; delete newBox["w-indexed"]; delete newBox["h-indexed"]; delete newBox["opacity-indexed"]; delete newBox._width; delete newBox._height; delete newBox.x; delete newBox.y; delete newBox.opacity; delete newBox.activeFrame; newBox.x = x; newBox.y = y; newBox._width = w; newBox._height = h; } } if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document._currentFrame, true); } } return retVal; }); private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); } onExternalDrop = (e: React.DragEvent) => { return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); } @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); const z = NumCast(refDoc.z); const x = (z ? xpo : xp) - docDragData.offset[0]; const y = (z ? ypo : yp) - docDragData.offset[1]; const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); const dropPos = this.Document._currentFrame !== undefined ? [dvals.x, dvals.y] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], this.Document._currentFrame, false); const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, this.Document.editScrollProgressivize ? vals.scroll : undefined, vals.opacity); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; } const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); !Cast(d, listSpec("string"), []).includes("background") && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); return true; } @undoBatch @action internalPdfAnnoDrop(e: Event, annoDragData: DragManager.PdfAnnoDragData, xp: number, yp: number) { const dragDoc = annoDragData.dropDocument; const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)]; dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]); dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]); annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation this.bringToFront(dragDoc); return true; } @undoBatch @action internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { if (linkDragData.linkSourceDocument === this.props.Document || this.props.Document.annotationOn) return false; if (!linkDragData.linkSourceDocument.context || StrCast(Cast(linkDragData.linkSourceDocument.context, Doc, null)?.type) === DocumentType.COL) { // const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); // this.props.addDocument(source); // linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed return false; } else { const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument(source); linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed e.stopPropagation(); return true; } } @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) { return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); } else if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) { return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) { return true; } return false; } pickCluster(probe: number[]) { return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1); if (grouping !== -1) { const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance; const cy = NumCast(cd.y) - this._clusterDistance; const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance; const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance; return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster; } return cluster; }, -1); } tryDragCluster(e: PointerEvent | TouchEvent, cluster: number) { if (cluster !== -1) { const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); if (ptsParent) { const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === cluster); const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); const de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { hideSource: !de.dropAction }); return true; } } return false; } @undoBatch @action updateClusters(_useClusters: boolean) { this.props.Document._useClusters = _useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document._useClusters) { const docFirst = docs[0]; docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); const preferredInd = NumCast(docFirst.cluster); docs.map(doc => doc.cluster = -1); docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { docFirst.cluster = i; } }))); if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { docFirst.cluster = preferredInd; } this._clusterSets.map((set, i) => { if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { docFirst.cluster = i; } }); if (docFirst.cluster === -1) { docs.map(doc => { doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); } childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); } } @undoBatch @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document._useClusters) { this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); const preferredInd = NumCast(doc.cluster); doc.cluster = -1; this._clusterSets.map((set, i) => set.map(member => { if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { doc.cluster = i; } })); if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } this._clusterSets.map((set, i) => { if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { doc.cluster = i; } }); if (doc.cluster === -1) { doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); this._clusterSets[doc.cluster].push(doc); } } } getClusterColor = (doc: Opt, renderDepth: number, property: string, layerProvider?: (doc: Doc, assign?: boolean) => boolean) => { let clusterColor = this.props.styleProvider?.(doc, this.props.renderDepth + 1, property, layerProvider); if (property !== "backgroundColor") return clusterColor; const cluster = NumCast(doc?.cluster); if (this.Document._useClusters) { if (this._clusterSets.length <= cluster) { setTimeout(() => doc && this.updateCluster(doc), 0); } else { // choose a cluster color from a palette const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; clusterColor = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document set && set.filter(s => !Cast(s.layers, listSpec("string"), []).includes("background")).map(s => clusterColor = StrCast(s.backgroundColor)); set && set.filter(s => Cast(s.layers, listSpec("string"), []).includes("background")).map(s => clusterColor = StrCast(s.backgroundColor)); } } else if (doc && NumCast(doc.group, -1) !== -1) clusterColor = "gray"; return clusterColor; } @action onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { return; } this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); if (e.button === 0 && (!e.shiftKey || this._hitCluster !== -1) && !e.altKey && !e.ctrlKey && this.props.active(true)) { // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { // this.props.addDocTab(this.props.Document, "replace"); // e.stopPropagation(); // e.preventDefault(); // return; // } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); // if not using a pen and in no ink mode if (Doc.GetSelectedTool() === InkTool.None) { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; } // eraser plus anything else mode else { e.stopPropagation(); e.preventDefault(); } } } @action handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.nativeEvent.cancelBubble) { // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = me.changedTouches[0]; if (pt) { this._hitCluster = this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)); if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); // if (Doc.SelectedTool() === InkTool.Highlighter || Doc.SelectedTool() === InkTool.Pen) { // e.stopPropagation(); // e.preventDefault(); // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); // this._points.push({ X: point[0], Y: point[1] }); // } if (Doc.GetSelectedTool() === InkTool.None) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); e.stopPropagation(); } else { e.preventDefault(); } } } } } @undoBatch onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { switch (ge.gesture) { case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); this.addDocument(inkDoc); e.stopPropagation(); break; case GestureUtils.Gestures.Box: const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] }; const bWidth = bounds.r - bounds.x; const bHeight = bounds.b - bounds.y; const sel = this.getActiveDocuments().filter(doc => { const l = NumCast(doc.x); const r = l + doc[WidthSym](); const t = NumCast(doc.y); const b = t + doc[HeightSym](); const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); if (pass) { doc.x = l - bounds.x - bWidth / 2; doc.y = t - bounds.y - bHeight / 2; } return pass; }); this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); sel.forEach(d => this.props.removeDocument(d)); e.stopPropagation(); break; case GestureUtils.Gestures.StartBracket: const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); this._inkToTextStartX = start[0]; this._inkToTextStartY = start[1]; break; case GestureUtils.Gestures.EndBracket: if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color); const sets = setDocs.map((sd) => { return Cast(sd.text, RichTextField)?.Text as string; }); if (sets.length && sets[0]) { this._wordPalette.clear(); const colors = setDocs.map(sd => FieldValue(sd.color) as string); sets.forEach((st: string, i: number) => { const words = st.split(","); words.forEach(word => { this._wordPalette.set(word, colors[i]); }); }); } const inks = this.getActiveDocuments().filter(doc => { if (doc.type === "ink") { const l = NumCast(doc.x); const r = l + doc[WidthSym](); const t = NumCast(doc.y); const b = t + doc[HeightSym](); const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); return pass; } return false; }); // const inkFields = inks.map(i => Cast(i.data, InkField)); const strokes: InkData[] = []; inks.forEach(i => { const d = Cast(i.data, InkField); const x = NumCast(i.x); const y = NumCast(i.y); const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); if (d) { strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); } }); CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { const wordResults = results.filter((r: any) => r.category === "inkWord"); for (const word of wordResults) { const indices: number[] = word.strokeIds; indices.forEach(i => { const otherInks: Doc[] = []; indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); inks[i].relatedInks = new List(otherInks); const uniqueColors: string[] = []; Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); inks[i].alternativeColors = new List(uniqueColors); if (this._wordPalette.has(word.recognizedText.toLowerCase())) { inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); } else if (word.alternates) { for (const alt of word.alternates) { if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); break; } } } }); } }); this._inkToTextStartX = end[0]; } break; case GestureUtils.Gestures.Text: if (ge.text) { const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); e.stopPropagation(); } } } _lastTap = 0; @action onPointerUp = (e: PointerEvent): void => { if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); } onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { runInAction(() => { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; }); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); e.preventDefault(); } this._lastTap = Date.now(); } } @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); this._lastX = e.clientX; this._lastY = e.clientY; } @action onPointerMove = (e: PointerEvent): void => { if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { if (this.props.active(true)) { e.stopPropagation(); } return; } if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { return; } if (!e.cancelBubble) { if (Doc.GetSelectedTool() === InkTool.None) { if (this.tryDragCluster(e, this._hitCluster)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); return; } (!MarqueeView.DragMarquee || e.altKey) && this.pan(e); } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { if (Doc.GetSelectedTool() === InkTool.None) { if (this.tryDragCluster(e, this._hitCluster)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); return; } this.pan(pt); } } // e.stopPropagation(); e.preventDefault(); } } handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { // pinch zooming if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; if (this.prevPoints.size === 2) { const oldPoint1 = this.prevPoints.get(pt1.identifier); const oldPoint2 = this.prevPoints.get(pt2.identifier); if (oldPoint1 && oldPoint2) { const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2); // if zooming, zoom if (dir !== 0) { const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2)); const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2)); const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; // calculate the raw delta value const rawDelta = (dir * (d1 + d2)); // this floors and ceils the delta value to prevent jitteriness const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8); this.zoom(centerX, centerY, delta * window.devicePixelRatio); this.prevPoints.set(pt1.identifier, pt1); this.prevPoints.set(pt2.identifier, pt2); } // this is not zooming. derive some form of panning from it. else { // use the centerx and centery as the "new mouse position" const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY); if (!this._pullDirection) { // if we are not bezel movement this.pan({ clientX: centerX, clientY: centerY }); } else { this._pullCoords = [centerX, centerY]; } this._lastX = centerX; this._lastY = centerY; } } } // e.stopPropagation(); e.preventDefault(); } } @action handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.nativeEvent.cancelBubble && this.props.active(true)) { // const pt1: React.Touch | null = e.targetTouches.item(0); // const pt2: React.Touch | null = e.targetTouches.item(1); // // if (!pt1 || !pt2) return; const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; if (pt1 && pt2) { const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this._lastX = centerX; this._lastY = centerY; const screenBox = this._mainCont?.getBoundingClientRect(); // determine if we are using a bezel movement if (screenBox) { if ((screenBox.right - centerX) < 100) { this._pullCoords = [centerX, centerY]; this._pullDirection = "right"; } else if (centerX - screenBox.left < 100) { this._pullCoords = [centerX, centerY]; this._pullDirection = "left"; } else if (screenBox.bottom - centerY < 100) { this._pullCoords = [centerX, centerY]; this._pullDirection = "bottom"; } else if (centerY - screenBox.top < 100) { this._pullCoords = [centerX, centerY]; this._pullDirection = "top"; } } this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); e.stopPropagation(); } } } cleanUpInteractions = () => { switch (this._pullDirection) { case "left": case "right": case "top": case "bottom": CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), this._pullDirection); } this._pullDirection = ""; this._pullCoords = [0, 0]; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); } @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); const invTransform = this.getLocalTransform().inverse(); if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { deltaScale = 1 / invTransform.Scale; } const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); } } @action onPointerWheel = (e: React.WheelEvent): void => { if (this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } else if (this.props.active(true)) { e.stopPropagation(); if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); else this.zoom(e.clientX, e.clientY, e.deltaY); } this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]); } @action setPan(panX: number, panY: number, panType: string = "None", clamp: boolean = false) { if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc, "")).map(doc => this.childDataProvider(doc, "")); if (measuredDocs.length) { const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content ({ xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) }, yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) } }) , { xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } }); const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()]; if (ranges.xrange.min >= (panX + panelDim[0] / 2)) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds else if (ranges.xrange.max <= (panX - panelDim[0] / 2)) panX = ranges.xrange.min - panelDim[0] / 2; if (ranges.yrange.min >= (panY + panelDim[1] / 2)) panY = ranges.yrange.max + panelDim[1] / 2; else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2; } } if (!this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.Document)) { this.Document._viewTransition = panType; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); this.Document._panX = this.isAnnotationOverlay ? newPanX : panX; this.Document._panY = this.isAnnotationOverlay ? newPanY : panY; } } bringToFront = action((doc: Doc, sendToBack?: boolean) => { if (sendToBack || Cast(doc.layers, listSpec("string"), []).includes("background")) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; } else { const docs = this.childLayoutPairs.map(pair => pair.layout); docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; zlast = docs.length + 1; } doc.zIndex = zlast + 1; } }); scaleAtPt(docpt: number[], scale: number) { const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); this.Document._viewTransition = "transform 500ms"; this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0]; this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; } focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, didFocus?: boolean) => { const state = HistoryUtil.getState(); // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; if (!init) { state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY }; HistoryUtil.pushState(state); } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) { init.panX = this.Document._panX; init.panY = this.Document._panY; HistoryUtil.pushState(state); } } SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { const annotOn = Cast(doc.annotationOn, Doc) as Doc; let delay = 1000; if (!annotOn) { !dontCenter && this.props.focus(doc); afterFocus && setTimeout(afterFocus, delay); } else { const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); const curScroll = NumCast(this.props.Document._scrollTop); let scrollTo = curScroll; if (curScroll + contextHgt < NumCast(doc.y)) { scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt; } else if (curScroll > NumCast(doc.y)) { scrollTo = Math.max(0, NumCast(doc.y) - 50); } if (curScroll !== scrollTo || this.props.Document._viewTransition) { this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo; delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0; !dontCenter && this.props.focus(this.props.Document); afterFocus && setTimeout(afterFocus, delay); } else { !dontCenter && delay && this.props.focus(this.props.Document); afterFocus?.(!dontCenter && delay ? true : false); } } } else { const layoutdoc = Doc.Layout(doc); const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; willZoom && this.setScaleToZoom(layoutdoc, scale); const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0); const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0); const newState = HistoryUtil.getState(); newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); if (DocListCast(this.dataDoc[this.props.annotationsKey || this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } Doc.BrushDoc(this.props.Document); const newDidFocus = didFocus || (newPanX !== savedState.px || newPanY !== savedState.py); const newAfterFocus = (didFocus: boolean) => { afterFocus && setTimeout(() => { // @ts-ignore if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) { this.Document._panX = savedState.px; this.Document._panY = savedState.py; this.Document[this.scaleFieldKey] = savedState.s; this.Document._viewTransition = savedState.pt; } }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0); return false; }; this.props.focus(this.props.Document, undefined, undefined, newAfterFocus, undefined, newDidFocus); Doc.linkFollowHighlight(doc); } } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { const pw = this.isAnnotationOverlay ? Doc.NativeWidth(this.props.Document) : this.props.PanelWidth(); const ph = this.isAnnotationOverlay ? Doc.NativeHeight(this.props.Document) : this.props.PanelHeight(); pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.active() || this.props.active()); } onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); backgroundHalo = computedFn(function (doc: Doc) { return BoolCast(this.Document._useClusters) || (NumCast(doc.group, -1) !== -1); }).bind(this); parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.props.parentActive?.(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, pinToPres: this.props.pinToPres, whenActiveChanged: this.props.whenActiveChanged, parentActive: this.parentActive, fitToBox: false, DataDoc: childData, Document: childLayout, LibraryPath: this.libraryPath, LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, setupDragLines: this.setupDragLines, dontRegisterView: this.props.dontRegisterView, rootSelected: childData ? this.rootSelected : returnFalse, dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, onClick: this.onChildClickHandler, onDoubleClick: this.onChildDoubleClickHandler, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, docRangeFilters: this.docRangeFilters, searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, styleProvider: this.getClusterColor, backgroundHalo: this.backgroundHalo, bringToFront: this.bringToFront, addDocTab: this.addDocTab, }; } addDocTab = action((doc: Doc, where: string) => { if (where === "inParent") { if (doc instanceof Doc) { const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; return this.props.addDocument(doc); } else { (doc as any as Doc[]).forEach(doc => { const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; }); return this.props.addDocument(doc); } } if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List(doc as any as Doc[]); return true; } return this.props.addDocTab(doc, where); }); getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); const { x, y, opacity } = this.Document._currentFrame === undefined ? params.pair.layout : CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), transition: StrCast(layoutDoc.dataTransition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: "" }; } viewDefsToJSX = (views: ViewDefBounds[]) => { return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); } onViewDefDivClick = (e: React.MouseEvent, payload: any) => { (this.props.viewDefDivClick || ScriptCast(this.props.Document.onViewDefDivClick))?.script.run({ this: this.props.Document, payload }); e.stopPropagation(); } private viewDefToJSX(viewDef: ViewDefBounds): Opt { const { x, y, z } = viewDef; const color = StrCast(viewDef.color); const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const transform = `translate(${x}px, ${y}px)`; if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below const fontSize = Cast(viewDef.fontSize, "string"); return [text, x, y].some(val => val === undefined) ? undefined : { ele:
{text}
, bounds: viewDef }; } else if (viewDef.type === "div") { return [x, y].some(val => val === undefined) ? undefined : { ele:
this.onViewDefDivClick(e, viewDef)} style={{ width, height, backgroundColor: color, transform }} />, bounds: viewDef }; } } childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc, replica: string) { return this._layoutPoolData.get(doc[Id] + (replica || "")); }.bind(this)); childSizeProvider = computedFn(function childSizeProvider(this: any, doc: Doc, replica: string) { return this._layoutSizeData.get(doc[Id] + (replica || "")); }.bind(this)); doEngineLayout(poolData: Map, engine: ( poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: ((views: ViewDefBounds[]) => ViewDefResult[])) => ViewDefResult[] ) { return engine(poolData, this.props.Document, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } doFreeformLayout(poolData: Map) { const layoutDocs = this.childLayoutPairs.map(pair => pair.layout); const initResult = this.Document.arrangeInit?.script.run({ docs: layoutDocs, collection: this.Document }, console.log); const state = initResult?.success ? initResult.result.scriptState : undefined; const elements = initResult?.success ? this.viewDefsToJSX(initResult.result.views) : []; this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => { const pos = this.getCalculatedPositions({ pair, index: i, collection: this.Document, docs: layoutDocs, state }); poolData.set(pair.layout[Id], pos); }); return elements; } @computed get doInternalLayoutComputation() { TraceMobx(); const newPool = new Map(); const engine = this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); switch (engine) { case "pass": return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }; case "timeline": return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; case "pivot": return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; case "starburst": return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) }; } return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } get doLayoutComputation() { const { newPool, computedElementData } = this.doInternalLayoutComputation; const array = Array.from(newPool.entries()); runInAction(() => { for (const entry of array) { const lastPos = this._cachedPool.get(entry[0]); // last computed pos const newPos = entry[1]; if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) { this._layoutPoolData.set(entry[0], newPos); } if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); } } }); this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => elements.push({ ele: , bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); if (this.props.isAnnotationOverlay) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @action componentDidMount() { super.componentDidMount?.(); this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); if (!this.props.annotationsKey) { this._boundsReaction = reaction(() => this.contentBounds, bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => { const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]); if (rbounds[0] !== bounds.x || rbounds[1] !== bounds.y || rbounds[2] !== bounds.r || rbounds[3] !== bounds.b) { this.Document._renderContentBounds = new List([bounds.x, bounds.y, bounds.r, bounds.b]); } })); } this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } componentWillUnmount() { this._layoutComputeReaction?.(); this._boundsReaction?.(); this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } elementFunc = () => this._layoutElements; @action onCursorMove = (e: React.PointerEvent) => { // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } //
@action onDragAutoScroll = (e: CustomEvent) => { if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; if (!this.props.Document._noAutoscroll && !this.props.renderDepth && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; if (deltaX !== 0 || deltaY !== 0) { this.Document._panY = NumCast(this.Document._panY) + deltaY / 2; this.Document._panX = NumCast(this.Document._panX) + deltaX / 2; } } e.stopPropagation(); } promoteCollection = undoBatch(action(() => { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = scr?.[0]; doc.y = scr?.[1]; }); this.props.addDocTab(childDocs as any as Doc, "inParent"); this.props.ContainingCollectionView?.removeDocument(this.props.Document); })); @undoBatch layoutDocsInGrid = action(() => { const docs = this.childLayoutPairs; const startX = this.Document._panX || 0; let x = startX; let y = this.Document._panY || 0; let i = 0; const width = Math.max(...docs.map(doc => NumCast(doc.layout._width))); const height = Math.max(...docs.map(doc => NumCast(doc.layout._height))); docs.forEach(pair => { pair.layout.x = x; pair.layout.y = y; x += width + 20; if (++i === 6) { i = 0; x = startX; y += height + 20; } }); }); @undoBatch @action toggleNativeDimensions = () => { Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); } @undoBatch @action toggleLockTransform = (): void => { this.layoutDoc._lockedTransform = this.layoutDoc._lockedTransform ? undefined : true; } private thumbIdentifier?: number; onContextMenu = (e: React.MouseEvent) => { if (this.props.annotationsKey || this.props.Document.annotationOn || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); !Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const viewctrls = ContextMenu.Instance.findByDescription("UI Controls..."); const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: "eye" }); this.props.ContainingCollectionView && optionItems.push({ description: "Move Items Out of Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); } !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } importDocument = (x: number, y: number) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; input.onchange = async _e => { const upload = Utils.prepend("/uploadDoc"); const formData = new FormData(); const file = input.files && input.files[0]; if (file) { formData.append('file', file); formData.append('remap', "true"); const response = await fetch(upload, { method: "POST", body: formData }); const json = await response.json(); if (json !== "error") { const doc = await DocServer.GetRefField(json); if (doc instanceof Doc) { const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); doc.x = xx, doc.y = yy; this.props.addDocument?.(doc); setTimeout(() => { SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { docs.docs.forEach(d => LinkManager.Instance.addLink(d)); }); }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. } } } }; input.click(); } @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, r2: { left: number, top: number, width: number, height: number }) { return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); } @action setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); if (activeDocs.length > 50) { DragManager.SetSnapLines([], []); return; } const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => { if (this.intersectRect(docDims(doc), rect)) { snappableDocs.push(doc); } }; const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to; const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; this.getActiveDocuments().filter(doc => !Cast(doc.layers, listSpec("string"), []).includes("background") && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs const horizLines: number[] = []; const vertLines: number[] = []; snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); const docSize = this.getTransform().inverse().transformDirection(width, height); horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]);// right line }); DragManager.SetSnapLines(horizLines, vertLines); } onPointerOver = (e: React.PointerEvent) => { if (SnappingManager.GetIsDragging()) { this.setupDragLines(e.ctrlKey || e.shiftKey); } e.stopPropagation(); } @observable private _hLines: number[] | undefined; @observable private _vLines: number[] | undefined; private childViews = () => { const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; return [ ...children, ...this.views, ]; } children = () => { const eles: JSX.Element[] = []; eles.push(...this.childViews()); eles.push(); return eles; } @computed get placeholder() { return
{this.props.Document.title?.toString()}
; } _nudgeTime = 0; nudge = action((x: number, y: number) => { if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); this._nudgeTime = Date.now(); setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document._viewTransition = undefined), 500); return true; } return false; }); chooseGridSpace = (gridSpace: number): number => { const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); } @computed get grid() { const gridSpace = this.chooseGridSpace(NumCast(this.layoutDoc["_backgroundGrid-spacing"], 50)); const shiftX = (this.props.isAnnotationOverlay ? 0 : -this.panX() % gridSpace - gridSpace) * this.zoomScaling(); const shiftY = (this.props.isAnnotationOverlay ? 0 : -this.panY() % gridSpace - gridSpace) * this.zoomScaling(); const renderGridSpace = gridSpace * this.zoomScaling(); const w = this.props.PanelWidth() + 2 * renderGridSpace; const h = this.props.PanelHeight() + 2 * renderGridSpace; return { const ctx = el?.getContext('2d'); if (ctx) { const Cx = this.centeringShiftX() % renderGridSpace; const Cy = this.centeringShiftY() % renderGridSpace; ctx.lineWidth = Math.min(1, Math.max(0.5, this.zoomScaling())); ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); ctx.clearRect(0, 0, w, h); if (ctx) { ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; ctx.beginPath(); for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { ctx.moveTo(x, Cy - h); ctx.lineTo(x, Cy + h); } for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { ctx.moveTo(Cx - w, y); ctx.lineTo(Cx + w, y); } ctx.stroke(); } } }} />; } trySelectCluster = (addToSel: boolean) => { if (this._hitCluster !== -1) { if (!addToSel) { SelectionManager.DeselectAll(); } const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster); this.selectDocuments(eles); return true; } return false; } @computed get marqueeView() { return 0 ? undefined : this.nudge} addDocTab={this.addDocTab} trySelectCluster={this.trySelectCluster} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
{this.layoutDoc["_backgroundGrid-show"] ? this.grid : (null)} {this.children}
{this.showTimeline ? : (null)}
; } @computed get contentScaling() { if (this.props.annotationsKey && !this.props.forceScaling) return 0; const nw = returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); const nh = returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); return
e.preventDefault()} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.backgroundEvents ? "all" : undefined, transform: this.contentScaling ? `scale(${this.contentScaling})` : "", transformOrigin: this.contentScaling ? "left top" : "", width: this.contentScaling ? `${100 / this.contentScaling}%` : "", height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} {!this.props.noOverlay ? : (null)}
{// uncomment to show snap lines
{this._hLines?.map(l => )} {this._vLines?.map(l => )}
}
; } } interface CollectionFreeFormOverlayViewProps { elements: () => ViewDefResult[]; } @observer class CollectionFreeFormOverlayView extends React.Component{ render() { return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); } } interface CollectionFreeFormViewPannableContentsProps { centeringShiftX: () => number; centeringShiftY: () => number; panX: () => number; panY: () => number; zoomScaling: () => number; viewDefDivClick?: ScriptField; children: () => JSX.Element[]; transition?: string; presPaths?: boolean; progressivize?: boolean; presPinView?: boolean; isAnnotationOverlay: boolean | undefined; } @observer class CollectionFreeFormViewPannableContents extends React.Component{ @observable private _drag: string = ''; //Adds event listener so knows pointer is down and moving onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); e.preventDefault(); const corner = e.target as any; if (corner) this._drag = corner.id; const rect = document.getElementById(this._drag); if (rect) { setupMoveUpEvents(e.target, e, this.onPointerMove, (e) => { }, (e) => { }); } } //Removes all event listeners onPointerUp = (e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); this._drag = ""; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } //Adjusts the value in NodeStore @action onPointerMove = (e: PointerEvent) => { const doc = document.getElementById('resizable'); const rect = doc!.getBoundingClientRect(); const toNumber = (original: number, delta: number): number => { return original + (delta * this.props.zoomScaling()); }; if (doc) { const height = doc.offsetHeight; const width = doc.offsetWidth; const top = doc.offsetTop; const left = doc.offsetLeft; switch (this._drag) { case "": break; case "resizer-br": doc.style.width = toNumber(width, e.movementX) + 'px'; doc.style.height = toNumber(height, e.movementY) + 'px'; break; case "resizer-bl": doc.style.width = toNumber(width, -e.movementX) + 'px'; doc.style.height = toNumber(height, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; break; case "resizer-tr": doc.style.width = toNumber(width, -e.movementX) + 'px'; doc.style.height = toNumber(height, -e.movementY) + 'px'; doc.style.top = toNumber(top, e.movementY) + 'px'; case "resizer-tl": doc.style.width = toNumber(width, -e.movementX) + 'px'; doc.style.height = toNumber(height, -e.movementY) + 'px'; doc.style.top = toNumber(top, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; case "resizable": doc.style.top = toNumber(top, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; } // this.updateAll(height, width, top, left); return false; } return true; } // scale: NumCast(targetDoc._viewScale), @computed get zoomProgressivizeContainer() { const activeItem = PresBox.Instance.activeItem; // const targetDoc = PresBox.Instance.targetDoc; if (activeItem && activeItem.presPinView && activeItem.id) { const vfLeft: number = NumCast(activeItem.presPinViewX); const vfTop: number = NumCast(activeItem.presPinViewY); const vfWidth: number = 100; const vfHeight: number = 100; return ( <> {!this.props.presPinView ? (null) :
} ); } } @computed get zoomProgressivize() { return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); } @computed get progressivize() { return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : (null); } @computed get presPaths() { const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden"); return !PresBox.Instance || !this.props.presPaths ? (null) : <>
{PresBox.Instance.order}
{PresBox.Instance.paths} ; } render() { // trace(); const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); return
{ const target = e.target as any; if (getComputedStyle(target)?.overflow === "visible") { // if collection is visible, then scrolling will mess things up since there are no scroll bars target.scrollTop = target.scrollLeft = 0; } }} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, transition: this.props.transition, width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection //willChange: "transform" }}> {this.props.children()} {this.presPaths} {this.progressivize} {this.zoomProgressivize}
; } }