diff options
Diffstat (limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 997 |
1 files changed, 398 insertions, 599 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 079a5d977..64398a60a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,60 +1,68 @@ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; +import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; +import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; +import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../../util/DragManager'; +import { DocUtils } from '../../../documents/DocUtils'; +import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { freeformScrollMode } from '../../../util/SettingsManager'; -import { SnappingManager } from '../../../util/SnappingManager'; +import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; -import { CtrlKey } from '../../GlobalKeyHandler'; -import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { InkingStroke } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; -import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; -import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; +import { DocumentView } from '../../nodes/DocumentView'; +import { FieldViewProps } from '../../nodes/FieldView'; +import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; -import { PinProps, PresBox } from '../../nodes/trails/PresBox'; -import { CreateImage } from '../../nodes/WebBoxRenderer'; -import { StyleProp } from '../../StyleProvider'; +import { OpenWhere } from '../../nodes/OpenWhere'; +import { PinDocView, PinProps } from '../../PinFuncs'; +import { StyleProp } from '../../StyleProp'; import { CollectionSubView } from '../CollectionSubView'; -import { TreeViewType } from '../CollectionTreeView'; +import { TreeViewType } from '../CollectionTreeViewType'; import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid'; -import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI'; +import { CollectionFreeFormClusters } from './CollectionFreeFormClusters'; import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents'; import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { + render() { + return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore + } +} export interface collectionFreeformViewProps { NativeWidth?: () => number; NativeHeight?: () => number; @@ -71,26 +79,21 @@ export interface collectionFreeformViewProps { @observer export class CollectionFreeFormView extends CollectionSubView<Partial<collectionFreeformViewProps>>() { public get displayName() { - return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')'; + return 'CollectionFreeFormView(' + (this.Document.title?.toString() ?? '') + ')'; } // this makes mobx trace() statements more descriptive - - @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); - @computed get paintFunc() { - const field = this.dataDoc[this.fieldKey]; - const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim(); - return !paintFunc - ? '' - : paintFunc.includes('dashDiv') - ? `const dashDiv = document.querySelector('#${this._paintedId}'); - (async () => { ${paintFunc} })()` - : paintFunc; + public unprocessedDocs: Doc[] = []; + public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>(); + public static from(dv?: DocumentView): CollectionFreeFormView | undefined { + const parent = CollectionFreeFormDocumentView.from(dv)?._props.parent; + return parent instanceof CollectionFreeFormView ? parent : undefined; } + + _oldWheel: any; + _clusters = new CollectionFreeFormClusters(this); constructor(props: any) { super(props); makeObservable(this); } - @observable - public static ShowPresPaths = false; private _panZoomTransitionTimer: any; private _lastX: number = 0; @@ -98,41 +101,48 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _downX: number = 0; private _downY: number = 0; private _downTime = 0; - private _clusterDistance: number = 75; - private _hitCluster: number = -1; private _disposers: { [name: string]: IReactionDisposer } = {}; private _renderCutoffData = observable.map<string, boolean>(); private _batch: UndoManager.Batch | undefined = undefined; private _brushtimer: any; private _brushtimer1: any; + private _eraserLock = 0; + private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. - public get isAnnotationOverlay() { - return this._props.isAnnotationOverlay; - } - public get scaleFieldKey() { - return (this._props.viewField ?? '') + '_freeform_scale'; - } - private get panXFieldKey() { - return (this._props.viewField ?? '') + '_freeform_panX'; - } - private get panYFieldKey() { - return (this._props.viewField ?? '') + '_freeform_panY'; - } - private get autoResetFieldKey() { - return (this._props.viewField ?? '') + '_freeform_autoReset'; - } + private get isAnnotationOverlay() { return this._props.isAnnotationOverlay; } // prettier-ignore + private get scaleFieldKey() { return (this._props.viewField ?? '') + '_freeform_scale'; } // prettier-ignore + private get panXFieldKey() { return (this._props.viewField ?? '') + '_freeform_panX'; } // prettier-ignore + private get panYFieldKey() { return (this._props.viewField ?? '') + '_freeform_panY'; } // prettier-ignore + private get autoResetFieldKey() { return (this._props.viewField ?? '') + '_freeform_autoReset'; } // prettier-ignore @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement @observable _showAnimTimeline = false; - @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeViewRef = React.createRef<MarqueeView>(); @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined = undefined; // highlighted region of freeform canvas used by presentations to indicate a region @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. + @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined; + @observable _lightboxDoc: Opt<Doc> = undefined; + @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); + @observable _keyframeEditing = false; + @computed get layoutEngine() { + return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); + } + @computed get childPointerEvents() { + const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine); + return SnappingManager.IsResizing + ? 'none' + : this._props.childPointerEvents?.() ?? + (this._props.viewDefDivClick || // + (engine === computePassLayout.name && !this._props.isSelected()) || + this.isContentActive() === false + ? 'none' + : this._props.pointerEvents?.()); + } @computed get contentViews() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele); @@ -154,14 +164,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay; } @computed get contentBounds() { - const cb = Cast(this.dataDoc.contentBounds, listSpec('number')); - return cb - ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] } - : aggregateBounds( - this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), - NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10), - NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10) - ); + return aggregateBounds( + this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), + NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10), + NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10) + ); } @computed get nativeWidth() { return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); @@ -192,31 +199,31 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .translate(-this.centeringShiftX, -this.centeringShiftY) .transform(this.panZoomXf); } + @computed get backgroundColor() { + return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor); + } + @computed get nativeDimScaling() { + if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1; + const nw = this.nativeWidth; + const nh = this.nativeHeight; + const hscale = nh ? this._props.PanelHeight() / nh : 1; + const wscale = nw ? this._props.PanelWidth() / nw : 1; + return wscale < hscale || (this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth) ? wscale : hscale; + } + @computed get paintFunc() { + const field = this.dataDoc[this.fieldKey]; + const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as FieldType)).trim(); + return !paintFunc + ? '' + : paintFunc.includes('dashDiv') + ? `const dashDiv = document.querySelector('#${this._paintedId}'); + (async () => { ${paintFunc} })()` + : paintFunc; + } public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true); } - public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) { - const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true); - const timecode = Math.round(time); - docs.forEach(doc => { - CollectionFreeFormDocumentView.animFields.forEach(val => { - const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); - }); - CollectionFreeFormDocumentView.animStringFields.forEach(val => { - const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); - }); - CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { - const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any); - }); - }); - return newTimer; - } - - _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. changeKeyFrame = (back = false) => { const currentFrame = Cast(this.Document._currentFrame, 'number', null); if (currentFrame === undefined) { @@ -227,19 +234,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], 1000); this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1); } else { - this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0); + this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0); this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1); this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame)); } }; - @observable _keyframeEditing = false; - @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); + @action setKeyFrameEditing = (set: boolean) => { + this._keyframeEditing = set; + }; getKeyFrameEditing = () => this._keyframeEditing; - onBrowseClickHandler = () => this._props.onBrowseClickScript?.() || ScriptCast(this.layoutDoc.onBrowseClick); + onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); elementFunc = () => this._layoutElements; viewTransition = () => (this._panZoomTransition ? '' + this._panZoomTransition : undefined); + panZoomTransition = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))); fitContentOnce = () => { const vals = this.fitToContentVals; this.layoutDoc._freeform_panX = vals.bounds.cx; @@ -251,41 +260,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); - zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); + zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`); ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this._props.isAnyChildContentActive(); addLiveTextBox = (newDoc: Doc) => { - FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newDoc); }; selectDocuments = (docs: Doc[]) => { - SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && SelectionManager.SelectView(dv, true)); + DocumentView.DeselectAll(); + docs.map(doc => DocumentView.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && DocumentView.SelectView(dv, true)); }; addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - if ((retVal = this._props.addDocument?.(newBox) || false)) { + retVal = this._props.addDocument?.(newBox) || false; + if (retVal) { this.bringToFront(newBox); - this.updateCluster(newBox); + this._clusters.addDocument(newBox); } } else { retVal = this._props.addDocument?.(newBox) || false; // bcz: deal with clusters } if (retVal) { - const newBoxes = newBox instanceof Doc ? [newBox] : newBox; - for (const newBox of newBoxes) { - if (newBox.activeFrame !== undefined) { - const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}_indexed`]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]); - delete newBox.activeFrame; - CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i])); + const newBoxes = toList(newBox); + newBoxes.forEach(box => { + if (box.activeFrame !== undefined) { + const vals = CollectionFreeFormDocumentView.animFields.map(field => box[field.key]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete box[`${field.key}_indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete box[field.key]); + delete box.activeFrame; + CollectionFreeFormDocumentView.animFields.forEach((field, i) => { + field.key !== 'opacity' && (box[field.key] = vals[i]); + }); } - } + }); if (this.Document._currentFrame !== undefined && !this._props.isAnnotationOverlay) { CollectionFreeFormDocumentView.setupKeyframes(newBoxes, NumCast(this.Document._currentFrame), true); } @@ -300,22 +312,63 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime); } + /** + * focuses on a specified point in the freeform coordinate space. (alternative to focusing on a Document) + * @param options + * @returns how long a transition it will be to focus on the point, or undefined the doc is a group or something else already moved + */ + focusOnPoint = (options: FocusViewOptions) => { + const { pointFocus, zoomTime, didMove } = options; + if (!this.Document.isGroup && pointFocus && !didMove) { + const dfltScale = this.isAnnotationOverlay ? 1 : 0.5; + if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) { + this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime); + options.didMove = true; + return zoomTime; + } + } + return undefined; + }; + + /** + * Focusing on a member of a group - + * Since groups can't pan and zoom like regular collections, this method focuses on a Doc in a group by + * focusing on the group with an additional transformation to force the final focus to be on the center of the group item. + * @param anchor + * @param options + * @returns + */ groupFocus = (anchor: Doc, options: FocusViewOptions) => { - options.docTransform = new Transform(-NumCast(this.layoutDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.layoutDoc[this.panYFieldKey]) + NumCast(anchor.y), 1); + if (options.pointFocus) return undefined; + options.docTransform = new Transform(NumCast(anchor.x) + NumCast(anchor._width)/2 - NumCast(this.layoutDoc[this.panXFieldKey]), + NumCast(anchor.y) + NumCast(anchor._height)/2- NumCast(this.layoutDoc[this.panYFieldKey]), 1); // prettier-ignore const res = this._props.focus(this.Document, options); options.docTransform = undefined; return res; }; - focus = (anchor: Doc, options: FocusViewOptions) => { - if (this._lightboxDoc) return; - if (anchor === this.Document) { - // if (options.willZoomCentered && options.zoomScale) { - // this.fitContentOnce(); - // options.didMove = true; - // } + /** + * focuses the freeform view on the anchor subject to options. + * If a pointFocus is specified, then groupFocus is triggered instad + * Otherwise, this shifts the pan and zoom to the anchor target (as specified by options). + * NOTE: focusing on a group only has an effet if the options contextPath is empty. + * @param anchor + * @param options + * @returns + */ + focus = (anchor: Doc, options: FocusViewOptions): any => { + if (anchor.isGroup && !options.docTransform && options.contextPath?.length) { + // don't focus on group if there's a context path because we're about to focus on a group item + // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement) + return undefined; + } + if (this._lightboxDoc) return undefined; + if (options.pointFocus) return this.focusOnPoint(options); + const anchorInCollection = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor); + const anchorInChildViews = this.childLayoutPairs.map(pair => pair.layout).includes(anchor); + if (!anchorInCollection && !anchorInChildViews) { + return undefined; } - if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) return; const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); @@ -331,13 +384,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.setPan(panX, panY, focusTime, 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 return focusTime; } + return undefined; }; getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> => new Promise<Opt<DocumentView>>(res => { if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false); - if (doc === this.Document) return res(this.DocumentView?.()); - const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv)); + if (doc === this.Document) { + res(this.DocumentView?.()); + return; + } + const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv)); findDoc(dv => res(dv)); }); @@ -352,7 +409,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .map(pair => pair.layout) .slice() .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - zsorted.forEach((doc, index) => (doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1)); + zsorted.forEach((doc, index) => { + doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1; + }); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; @@ -378,7 +437,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront } - (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); + (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this._clusters.addDocuments(docDragData.droppedDocuments); return true; } @@ -405,7 +464,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection let added = false; // do nothing if link is dropped into any freeform view parent of dragged document const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }); - added = this._props.addDocument?.(source) ? true : false; + added = !!this._props.addDocument?.(source); de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed if (de.complete.linkDocument) { de.complete.linkDocument.layout_isSvg = true; @@ -420,186 +479,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData); - else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); - else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); + if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.screenToFreeformContentsXf.transformPoint(e.pageX, e.pageY)); - static overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { - const doc2Layout = Doc.Layout(doc2); - const doc1Layout = Doc.Layout(doc1); - const x2 = NumCast(doc2.x) - clusterDistance; - const y2 = NumCast(doc2.y) - clusterDistance; - const w2 = NumCast(doc2Layout._width) + clusterDistance; - const h2 = NumCast(doc2Layout._height) + clusterDistance; - const x = NumCast(doc1.x) - clusterDistance; - const y = NumCast(doc1.y) - clusterDistance; - const w = NumCast(doc1Layout._width) + clusterDistance; - const h = NumCast(doc1Layout._height) + clusterDistance; - return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 }); - } - pickCluster(probe: number[]) { - return this.childLayoutPairs - .map(pair => pair.layout) - .reduce((cluster, cd) => { - const grouping = this.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1); - if (grouping !== -1) { - const layoutDoc = Doc.Layout(cd); - const cx = NumCast(cd.x) - this._clusterDistance / 2; - const cy = NumCast(cd.y) - this._clusterDistance / 2; - const cw = NumCast(layoutDoc._width) + this._clusterDistance; - const ch = NumCast(layoutDoc._height) + 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, cluster: number) { - if (cluster !== -1) { - const ptsParent = e; - if (ptsParent) { - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster); - const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.DocumentView?.())!); - const { left, top } = clusterDocs[0].getBounds || { left: 0, top: 0 }; - const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? dropActionType.embed : undefined); - de.moveDocument = this._props.moveDocument; - de.offset = this.screenToFreeformContentsXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); - DragManager.StartDocumentDrag( - clusterDocs.map(v => v.ContentDiv!), - de, - ptsParent.clientX, - ptsParent.clientY, - { hideSource: !de.dropAction } - ); - return true; - } - } - - return false; - } - - @action - updateClusters(_freeform_useClusters: boolean) { - this.Document._freeform_useClusters = _freeform_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.Document._freeform_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.layout_cluster); - docs.map(doc => (doc.layout_cluster = -1)); - docs.map(doc => - this._clusterSets.map((set, i) => - set.map(member => { - if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { - docFirst.layout_cluster = i; - } - }) - ) - ); - if ( - docFirst.layout_cluster === -1 && - preferredInd !== -1 && - this._clusterSets.length > preferredInd && - (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) - ) { - docFirst.layout_cluster = preferredInd; - } - this._clusterSets.map((set, i) => { - if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { - docFirst.layout_cluster = i; - } - }); - if (docFirst.layout_cluster === -1) { - docs.map(doc => { - doc.layout_cluster = this._clusterSets.length; - this._clusterSets.push([doc]); - }); - } else if (this._clusterSets.length) { - for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); - docs.map(doc => this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc)); - } - childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child)); - } - } - - @action - updateCluster = (doc: Doc) => { - const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.Document._freeform_useClusters) { - this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); - const preferredInd = NumCast(doc.layout_cluster); - doc.layout_cluster = -1; - this._clusterSets.forEach((set, i) => - set.forEach(member => { - if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { - doc.layout_cluster = i; - } - }) - ); - if (doc.layout_cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { - doc.layout_cluster = preferredInd; - } - this._clusterSets.forEach((set, i) => { - if (doc.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { - doc.layout_cluster = i; - } - }); - if (doc.layout_cluster === -1) { - doc.layout_cluster = this._clusterSets.length; - this._clusterSets.push([doc]); - } else if (this._clusterSets.length) { - for (let i = this._clusterSets.length; i <= doc.layout_cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); - this._clusterSets[doc.layout_cluster ?? 0].push(doc); - } - } - }; - - clusterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => { - let styleProp = this._props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 - if (doc && this.childDocList?.includes(doc)) - switch (property.split(':')[0]) { - case StyleProp.BackgroundColor: - const cluster = NumCast(doc?.layout_cluster); - if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) { - if (this._clusterSets.length <= cluster) { - setTimeout(() => doc && this.updateCluster(doc)); - } 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)']; - styleProp = 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?.map(s => (styleProp = StrCast(s.backgroundColor))); - } - } - break; - case StyleProp.FillColor: - if (doc && this.Document._currentFrame !== undefined) { - return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor; - } - } - return styleProp; - }; - - trySelectCluster = (addToSel: boolean) => { - if (addToSel && this._hitCluster !== -1) { - !addToSel && SelectionManager.DeselectAll(); - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster); - this.selectDocuments(eles); - return true; - } - return false; - }; - @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; @@ -620,27 +506,33 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection break; case InkTool.None: if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { - this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false); + const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false); } break; + default: } } } }; - public unprocessedDocs: Doc[] = []; - public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>(); @undoBatch onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { switch (ge.gesture) { - default: - case GestureUtils.Gestures.Line: - case GestureUtils.Gestures.Circle: - case GestureUtils.Gestures.Rectangle: - case GestureUtils.Gestures.Triangle: - case GestureUtils.Gestures.Stroke: - const points = ge.points; + case Gestures.Text: + if (ge.text) { + const B = this.screenToFreeformContentsXf.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(); + } + break; + case Gestures.Line: + case Gestures.Circle: + case Gestures.Rectangle: + case Gestures.Triangle: + case Gestures.Stroke: + default: { + const { points } = ge; const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; const inkDoc = Docs.Create.InkDocument( @@ -659,29 +551,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } this.addDocument(inkDoc); e.stopPropagation(); - break; - case GestureUtils.Gestures.Rectangle: - const strokes = this.getActiveDocuments() - .filter(doc => doc.type === DocumentType.INK) - .map(i => { - const d = Cast(i.stroke, InkField); - const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); - const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); - return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y })); - }); - - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {}); - break; - case GestureUtils.Gestures.Text: - if (ge.text) { - const B = this.screenToFreeformContentsXf.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(); - } + } } }; @action - onEraserUp = (e: PointerEvent): void => { + onEraserUp = (): void => { this._deleteList.forEach(ink => ink._props.removeDocument?.(ink.Document)); this._deleteList = []; this._batch?.end(); @@ -690,12 +564,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onClick = (e: React.MouseEvent) => { if (this._lightboxDoc) this._lightboxDoc = undefined; - if (Utils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) { - if (this.onBrowseClickHandler()) { - this.onBrowseClickHandler().script.run({ documentView: this.DocumentView?.(), clientX: e.clientX, clientY: e.clientY }); - e.stopPropagation(); - e.preventDefault(); - } else if (this.isContentActive() && e.shiftKey) { + if (ClientUtils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) { + if (this.isContentActive() && e.shiftKey) { // reset zoom of freeform view to 1-to-1 on a shift + double click this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY), 1); e.stopPropagation(); @@ -704,9 +574,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - @action scrollPan = (e: WheelEvent | { deltaX: number; deltaY: number }): void => { - PresBox.Instance?.pauseAutoPres(); + SnappingManager.TriggerUserPanned(); this.setPan(NumCast(this.Document[this.panXFieldKey]) - e.deltaX, NumCast(this.Document[this.panYFieldKey]) - e.deltaY, 0, true); }; @@ -714,7 +583,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection pan = (e: PointerEvent): void => { const ctrlKey = e.ctrlKey && !e.shiftKey; const shiftKey = e.shiftKey && !e.ctrlKey; - PresBox.Instance?.pauseAutoPres(); + SnappingManager.TriggerUserPanned(); this.DocumentView?.().clearViewTransition(); const [dxi, dyi] = this.screenToFreeformContentsXf.transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); const { x: dx, y: dy } = Utils.rotPt(dxi, dyi, this.ScreenToLocalBoxXf().Rotate); @@ -723,7 +592,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._lastY = e.clientY; }; - _eraserLock = 0; /** * Erases strokes by intersecting them with an invisible "eraser stroke". * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, @@ -746,7 +614,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection segments.forEach(segment => this.forceStrokeGesture( e, - GestureUtils.Gestures.Stroke, + Gestures.Stroke, segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) ) ); @@ -759,12 +627,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); return false; }; - forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => { + forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => { this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text)); }; onPointerMove = (e: PointerEvent) => { - if (this.tryDragCluster(e, this._hitCluster)) { + if (this._clusters.tryToDrag(e)) { e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over return true; } @@ -783,26 +651,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) }; const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; - - return this.childDocs - .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) + // prettier-ignore + return this.childDocs + .map(doc => DocumentView.getDocumentView(doc, this.DocumentView?.())) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) - .map(inkView => ({ inkViewBounds: inkView!.getBounds, inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) - .filter( - ({ inkViewBounds }) => + .map(inkView => inkView!) + .map(inkView => ({ inkViewBounds: inkView.getBounds, inkStroke: inkView.ComponentView as InkingStroke, inkView })) + .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom && eraserMax.X >= inkViewBounds.left && - eraserMax.Y >= inkViewBounds.top - ) + eraserMax.Y >= inkViewBounds.top) .reduce( (intersections, { inkStroke, inkView }) => { const { inkData } = inkStroke.inkScaledData(); // Convert from screen space to ink space for the intersection. const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); const currPointInkSpace = inkStroke.ptFromScreen(currPoint); - for (var i = 0; i < inkData.length - 3; i += 4) { + for (let i = 0; i < inkData.length - 3; i += 4) { const rawIntersects = InkField.Segment(inkData, i).intersects({ // compute all unique intersections p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, @@ -828,16 +695,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => { const segments: Segment[] = []; - var segment: Segment = []; - var startSegmentT = 0; + let segment: Segment = []; + let startSegmentT = 0; const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData(); // This iterates through all segments of the curve and splits them where they intersect another curve. // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted) - for (var i = 0; i < inkData.length - 3; i += 4) { + for (let i = 0; i < inkData.length - 3; i += 4) { const inkSegment = InkField.Segment(inkData, i); // Getting all t-value intersections of the current curve with all other curves. const tVals = this.getInkIntersections(i, ink, inkSegment).sort(); if (tVals.length) { + // eslint-disable-next-line no-loop-func tVals.forEach((t, index) => { const docCurveTVal = t + Math.floor(i / 4); if (excludeT < startSegmentT || excludeT > docCurveTVal) { @@ -886,13 +754,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.childDocs .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) .forEach(doc => { - const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke; + const otherInk = DocumentView.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke; const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point)); const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt)); - for (var j = 0; j < otherCtrlPts.length - 3; j += 4) { + for (let j = 0; j < otherCtrlPts.length - 3; j += 4) { const neighboringSegment = i === j || i === j - 4 || i === j + 4; // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. + // eslint-disable-next-line no-continue if (ink?.Document === otherInk.Document && neighboringSegment) continue; const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y }))); @@ -903,7 +772,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) { tVals.push(apt.t); } - this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => { + this.bintersects(curve, otherCurve).forEach((val: string | number /* , i: number */) => { // Converting the Bezier.js Split type to a t-value number. const t = +val.toString().split('/')[0]; if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical). @@ -948,7 +817,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom - PresBox.Instance?.pauseAutoPres(); + SnappingManager.TriggerUserPanned(); if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return; e.stopPropagation(); const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); @@ -956,7 +825,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection switch ( !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey ?// Doc.UserDoc().freeformScrollMode : // no modifiers, do assigned mode - e.ctrlKey && !CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan + e.ctrlKey && !SnappingManager.CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan freeformScrollMode.Zoom : freeformScrollMode.Pan // prettier-ignore ) { case freeformScrollMode.Pan: @@ -966,8 +835,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.scrollPan({ deltaX: -deltaX * this.screenToFreeformContentsXf.Scale, deltaY: e.shiftKey ? 0 : -deltaY * this.screenToFreeformContentsXf.Scale }); break; } - default: + // eslint-disable-next-line no-fallthrough case freeformScrollMode.Zoom: + default: if ((e.ctrlKey || !scrollable) && this._props.isContentActive()) { this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this._props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? // e.preventDefault(); @@ -977,7 +847,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { + setPan(panXIn: number, panYIn: number, panTime: number = 0, clamp: boolean = false) { + let panX = panXIn; + let panY = panYIn; // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code. if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction(); @@ -1029,14 +901,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection scale * NumCast(this.dataDoc._panY_max, nativeHeight) + (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); - if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) { - const relTop = NumCast(this.layoutDoc.layout_scrollTop) / maxScrollTop; - this.layoutDoc.layout_scrollTop = undefined; - newPanY = minPanY + relTop * (maxPanY - minPanY); - } else if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) { - const maxPanY = minPanY + fitYscroll; - const relTop = (panY - minPanY) / (maxPanY - minPanY); - setTimeout(() => (this.layoutDoc.layout_scrollTop = relTop * maxScrollTop), 10); + if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) { + const maxPanScrollY = minPanY + fitYscroll; + const relTop = (panY - minPanY) / (maxPanScrollY - minPanY); + setTimeout(() => { + this.layoutDoc.layout_scrollTop = relTop * maxScrollTop; + }, 10); newPanY = minPanY; } !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); @@ -1088,7 +958,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._panZoomTransition = transitionTime; this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer); this._panZoomTransitionTimer = setTimeout( - action(() => (this._panZoomTransition = 0)), + action(() => { + this._panZoomTransition = 0; + }), transitionTime ); }; @@ -1161,7 +1033,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection newDoc[DocData][Doc.LayoutFieldKey(newDoc, fieldProps.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); - FormattedTextBox.SetSelectOnLoad(newDoc); + Doc.SetSelectOnLoad(newDoc); FormattedTextBox.DontSelectInitialText = true; return this.addDocument?.(newDoc); }, 'copied text note'); @@ -1171,20 +1043,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation?.(); return this.createTextDocCopy(fieldProps, !e.altKey && e.key !== 'Tab'); } + return undefined; }; - @computed get childPointerEvents() { - const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine); - return SnappingManager.IsResizing - ? 'none' - : this._props.childPointerEvents?.() ?? - (this._props.viewDefDivClick || // - (engine === computePassLayout.name && !this._props.isSelected()) || - this.isContentActive() === false - ? 'none' - : this._props.pointerEvents?.()); - } - - @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined; childPointerEventsFunc = () => this._childPointerEvents; childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { @@ -1192,20 +1052,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const childData = entry.pair.data; return ( <CollectionFreeFormDocumentView + // eslint-disable-next-line react/jsx-props-no-spreading {...OmitKeys(entry, ['replica', 'pair']).omit} key={childLayout[Id] + (entry.replica || '')} Document={childLayout} + parent={this} containerViewPath={this.DocumentView?.().docViewPath} - styleProvider={this.clusterStyleProvider} + styleProvider={this._clusters.styleProvider} TemplateDataDocument={childData} dragStarting={this.dragStarting} dragEnding={this.dragEnding} + isAnyChildContentActive={this.isAnyChildContentActive} isGroupActive={this._props.isGroupActive} renderDepth={this._props.renderDepth + 1} hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)} - suppressSetHeight={this.layoutEngine ? true : false} + suppressSetHeight={!!this.layoutEngine} RenderCutoffProvider={this.renderCutoffProvider} - CollectionFreeFormView={this} LayoutTemplate={childLayout.z ? undefined : this._props.childLayoutTemplate} LayoutTemplateString={childLayout.z ? undefined : this._props.childLayoutString} rootSelected={childData ? this.rootSelected : returnFalse} @@ -1213,7 +1075,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onClickScript={this.onChildClickHandler} onKey={this.onKeyDown} onDoubleClickScript={this.onChildDoubleClickHandler} - onBrowseClickScript={this.onBrowseClickHandler} bringToFront={this.bringToFront} ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf} PanelWidth={childLayout[Width]} @@ -1237,39 +1098,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /> ); } - addDocTab = action((doc: Doc, where: OpenWhere) => { - if (this._props.isAnnotationOverlay) return this._props.addDocTab(doc, where); + addDocTab = action((docsIn: Doc | Doc[], where: OpenWhere) => { + const docs = toList(docsIn); + if (this._props.isAnnotationOverlay) return this._props.addDocTab(docs, where); switch (where) { case OpenWhere.inParent: - return this._props.addDocument?.(doc) || false; - case OpenWhere.inParentFromScreen: - const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer); + return this._props.addDocument?.(docs) || false; + case OpenWhere.inParentFromScreen: { + const docContext = DocCast(docs[0]?.embedContainer); return ( (this.addDocument?.( - (doc instanceof Doc ? [doc] : doc).map(doc => { - const pt = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - doc.x = pt[0]; - doc.y = pt[1]; + toList(docs).map(doc => { + [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); return doc; }) ) && (!docContext || this._props.removeDocument?.(docContext))) || false ); + } case undefined: case OpenWhere.lightbox: - if (this.layoutDoc._isLightbox) { - this._lightboxDoc = doc; - return true; - } - if (doc === this.Document || this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc)) { - if (doc.hidden) doc.hidden = false; - return true; + { + const firstDoc = docs[0]; + if (this.layoutDoc._isLightbox) { + this._lightboxDoc = firstDoc; + return true; + } + if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) { + if (firstDoc.hidden) firstDoc.hidden = false; + return true; + } } + break; + default: } - return this._props.addDocTab(doc, where); + return this._props.addDocTab(docs, where); }); - @observable _lightboxDoc: Opt<Doc> = undefined; getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData { const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); @@ -1287,16 +1152,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const rotation = Cast(_rotation,'number', !this.layoutDoc._rotation_jitter ? null : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) ); - const childProps = { ...this._props, fieldKey: '', styleProvider: this.clusterStyleProvider }; + const childProps = { ...this._props, fieldKey: '', styleProvider: this._clusters.styleProvider }; return { - x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x), - y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y), + x: isNaN(NumCast(x)) ? 0 : NumCast(x), + y: isNaN(NumCast(y)) ? 0 : NumCast(y), z: Cast(z, 'number'), autoDim, rotation, - color: Cast(color, 'string') ? StrCast(color) : this.clusterStyleProvider(childDoc, childProps, StyleProp.Color), - backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.clusterStyleProvider(childDoc, childProps, StyleProp.BackgroundColor), - opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.clusterStyleProvider?.(childDoc, childProps, StyleProp.Opacity), + color: Cast(color, 'string') ? StrCast(color) : this._clusters.styleProvider(childDoc, childProps, StyleProp.Color), + backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this._clusters.styleProvider(childDoc, childProps, StyleProp.BackgroundColor), + opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this._clusters.styleProvider?.(childDoc, childProps, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), width: _width, height: _height, @@ -1312,9 +1177,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); }; - viewDefsToJSX = (views: ViewDefBounds[]) => { - return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); - }; + viewDefsToJSX = (views: ViewDefBounds[]) => (!Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!)); viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { const { x, y, z } = viewDef; @@ -1335,7 +1198,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ), bounds: viewDef, }; - } else if (viewDef.type === 'div') { + } + if (viewDef.type === 'div') { return [x, y].some(val => val === undefined) ? undefined : { @@ -1351,13 +1215,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection bounds: viewDef, }; } + return undefined; } - renderCutoffProvider = computedFn( - function renderCutoffProvider(this: any, doc: Doc) { - return this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + ''); - }.bind(this) - ); + /** + * Determines whether the passed doc should be rendered + * since rendering a large collection of documents can be slow, at startup, docs are rendered in batches. + * each doc's render() method will call the cutoff provider which will let the doc know if it should render itself yet, or wait + */ + renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + ''))); doEngineLayout( poolData: Map<string, PoolData>, @@ -1367,27 +1233,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } doFreeformLayout(poolData: Map<string, PoolData>) { - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); return [] as ViewDefResult[]; } - @computed get layoutEngine() { - return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); - } @computed get doInternalLayoutComputation() { TraceMobx(); const newPool = new Map<string, PoolData>(); - // prettier-ignore switch (this.layoutEngine) { case computePassLayout.name : return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) }; case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) }; - } - return { newPool, computedElementData: this.doFreeformLayout(newPool) }; + default: return { newPool, computedElementData: this.doFreeformLayout(newPool) }; + } // prettier-ignore } - @action doLayoutComputation = (newPool: Map<string, PoolData>, computedElementData: ViewDefResult[]) => { const elements = computedElementData.slice(); Array.from(newPool.entries()) @@ -1400,7 +1261,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }) ); - this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); + this._clusters.initLayout(); return elements; }; @@ -1412,7 +1273,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection presentation_transition: 500, annotationOn: this.Document, }); - PresBox.pinDocView( + PinDocView( anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document @@ -1428,8 +1289,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return anchor; }; - @action closeInfo = () => (Doc.IsInfoUIDisabled = true); - infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />); + childDocsFunc = () => this.childDocs; + closeInfo = action(() => { Doc.IsInfoUIDisabled = true }); // prettier-ignore + static _infoUI: ((doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => JSX.Element) | null = null; + static SetInfoUICreator(func: (doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => JSX.Element) { + CollectionFreeFormView._infoUI = func; + } + infoUI = () => + Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth + ? null // + : CollectionFreeFormView._infoUI?.(this.Document, this.layoutDoc, this.childDocsFunc, this.closeInfo) || null; componentDidMount() { this._props.setContentViewBox?.(this); @@ -1470,7 +1339,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.pointerevents = reaction( () => this.childPointerEvents, - pointerevents => (this._childPointerEvents = pointerevents as any), + pointerevents => { + this._childPointerEvents = pointerevents as any; + }, { fireImmediately: true } ); @@ -1487,6 +1358,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!code.includes('dashDiv')) { const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true }); if (script.compiled) script.run({ this: this.DocumentView?.() }); + // eslint-disable-next-line no-eval } else code && !first && eval?.(code); }, { fireImmediately: true } @@ -1495,45 +1367,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.layoutElements = reaction( // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters () => this.doInternalLayoutComputation, - computation => (this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData)), + computation => { + this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData); + }, { fireImmediately: true } ); } - static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { - if (oldDiv.childNodes && newDiv) { - for (let i = 0; i < oldDiv.childNodes.length; i++) { - this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); - } - } - if (oldDiv instanceof HTMLCanvasElement) { - if (oldDiv.className === 'collectionFreeFormView-grid') { - const newCan = newDiv as HTMLCanvasElement; - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(document.createElement('div')); - } else { - const canvas = oldDiv; - const img = document.createElement('img'); // create a Image Element - try { - img.src = canvas.toDataURL(); //image source - } catch (e) { - console.log(e); - } - img.style.width = canvas.style.width; - img.style.height = canvas.style.height; - const newCan = newDiv as HTMLCanvasElement; - if (newCan) { - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(img); - } - } - } + componentWillUnmount() { + this.dataDoc[this.autoResetFieldKey] && this.resetView(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } updateIcon = () => - CollectionFreeFormView.UpdateIcon( + UpdateIcon( this.layoutDoc[Id] + '-icon' + new Date().getTime(), this.DocumentView?.().ContentDiv!, NumCast(this.layoutDoc._width), @@ -1551,55 +1398,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } ); - public static UpdateIcon( - filename: string, - docViewContent: HTMLElement, - width: number, - height: number, - panelWidth: number, - panelHeight: number, - scrollTop: number, - realNativeHeight: number, - noSuffix: boolean, - replaceRootFilename: string | undefined, - cb: (iconFile: string, nativeWidth: number, nativeHeight: number) => any - ) { - const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = width.toString(); - newDiv.style.height = height.toString(); - this.replaceCanvases(docViewContent, newDiv); - const htmlString = new XMLSerializer().serializeToString(newDiv); - const nativeWidth = width; - const nativeHeight = height; - return CreateImage(Utils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight) - .then(async (data_url: any) => { - const returnedFilename = await Utils.convertDataUri(data_url, filename, noSuffix, replaceRootFilename); - cb(returnedFilename as string, nativeWidth, nativeHeight); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); - } - - componentWillUnmount() { - this.dataDoc[this.autoResetFieldKey] && this.resetView(); - Object.values(this._disposers).forEach(disposer => disposer?.()); - } - - @action - onCursorMove = (e: React.PointerEvent) => { + onCursorMove = () => { // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); }; @undoBatch promoteCollection = () => { const childDocs = this.childDocs.slice(); - childDocs.forEach(doc => { + childDocs.forEach(docIn => { + const doc = docIn; const scr = this.screenToFreeformContentsXf.inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = scr?.[0]; doc.y = scr?.[1]; }); - this._props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen); + this._props.addDocTab(childDocs, OpenWhere.inParentFromScreen); }; @undoBatch @@ -1608,7 +1420,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const width = Math.max(...docs.map(doc => NumCast(doc._width))) + 20; const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20; const dim = Math.ceil(Math.sqrt(docs.length)); - docs.forEach((doc, i) => { + docs.forEach((docIn, i) => { + const doc = docIn; doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2; doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2; }); @@ -1641,7 +1454,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - onContextMenu = (e: React.MouseEvent) => { + onContextMenu = () => { if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription('Appearance...'); @@ -1652,7 +1465,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); return; } - !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); + !Doc.noviceMode && + Doc.UserDoc().defaultTextLayout && + appearanceItems.push({ + description: 'Reset default note style', + event: () => { + Doc.UserDoc().defaultTextLayout = undefined; + }, + icon: 'eye', + }); appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); @@ -1660,15 +1481,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null; - !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; + !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = ContextMenu.Instance.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; !this._props.isAnnotationOverlay && !Doc.noviceMode && - optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' }); - 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' }); + optionItems.push({ + description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', + event: action(() => { + this._showAnimTimeline = !this._showAnimTimeline; + }), + icon: 'eye', + }); + 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', + }); this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' }); @@ -1707,15 +1541,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect); const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - activeDocs - .filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc)) - .forEach(doc => DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); + activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); const horizLines: number[] = []; const vertLines: number[] = []; const invXf = this.screenToFreeformContentsXf.inverse(); snappableDocs - .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc)))) + .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = invXf.transformPoint(left, top); @@ -1731,29 +1563,41 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRender = action(() => { if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) { - const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); + const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5; - for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) { - this._renderCutoffData.set(layout_unrendered[i][Id] + '', true); + for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) { + this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true); } } this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1); }); - @computed get placeholder() { - return ( - <div className="collectionfreeformview-placeholder" style={{ background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) }}> - <span className="collectionfreeformview-placeholderSpan">{this.Document.annotationOn ? '' : this.Document.title?.toString()}</span> - </div> - ); - } - + showPresPaths = () => SnappingManager.ShowPresPaths; brushedView = () => this._brushedView; - gridColor = () => - DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor))) - .fade(0.5) - .toString(); - @computed get backgroundGrid() { + gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore + nativeDim = () => this.nativeDimScaling; + + brushView = action((viewport: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number = 2500) => { + this._brushtimer1 && clearTimeout(this._brushtimer1); + this._brushtimer && clearTimeout(this._brushtimer); + this._brushedView = undefined; + this._brushtimer1 = setTimeout( + action(() => { + this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 }; + this._brushtimer = setTimeout(action(() => { this._brushedView = undefined; }), holdTime); // prettier-ignore + }), + transTime + 1 + ); + }); + lightboxPanelWidth = () => Math.max(0, this._props.PanelWidth() - 30); + lightboxPanelHeight = () => Math.max(0, this._props.PanelHeight() - 30); + lightboxScreenToLocal = () => this.ScreenToLocalBoxXf().translate(-15, -15); + onPassiveWheel = (e: WheelEvent) => { + const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling; + this._props.isSelected() && !scrollable && e.preventDefault(); + }; + get backgroundGrid() { return ( <div> <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render when taking snapshot of a dashboard and the background grid is on!!? @@ -1780,7 +1624,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection brushedView={this.brushedView} isAnnotationOverlay={this.isAnnotationOverlay} transform={this.PanZoomCenterXf} - transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))} + showPresPaths={this.showPresPaths} + transition={this.panZoomTransition} viewDefDivClick={this._props.viewDefDivClick}> {this.props.children ?? null} {/* most likely case of children is document content that's being annoated: eg., an image */} {this.contentViews} @@ -1797,7 +1642,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} slowLoadDocuments={this.slowLoadDocuments} - trySelectCluster={this.trySelectCluster} + trySelectCluster={this._clusters.tryToSelect} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} @@ -1813,39 +1658,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection </MarqueeView> ); } - - @computed get nativeDimScaling() { - if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1; - const nw = this.nativeWidth; - const nh = this.nativeHeight; - const hscale = nh ? this._props.PanelHeight() / nh : 1; - const wscale = nw ? this._props.PanelWidth() / nw : 1; - return wscale < hscale || (this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth) ? wscale : hscale; - } - nativeDim = () => this.nativeDimScaling; - - @action - brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number = 2500) => { - this._brushtimer1 && clearTimeout(this._brushtimer1); - this._brushtimer && clearTimeout(this._brushtimer); - this._brushedView = undefined; - this._brushtimer1 = setTimeout( - action(() => { - this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 }; - this._brushtimer = setTimeout(action(() => (this._brushedView = undefined)), holdTime); // prettier-ignore - }), - transTime + 1 + get placeholder() { + return ( + <div className="collectionfreeformview-placeholder" style={{ background: this.backgroundColor }}> + <span className="collectionfreeformview-placeholderSpan">{this.Document.annotationOn ? '' : this.Document.title?.toString()}</span> + </div> ); - }; - lightboxPanelWidth = () => Math.max(0, this._props.PanelWidth() - 30); - lightboxPanelHeight = () => Math.max(0, this._props.PanelHeight() - 30); - lightboxScreenToLocal = () => this.ScreenToLocalBoxXf().translate(-15, -15); - onPassiveWheel = (e: WheelEvent) => { - const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); - const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling; - this._props.isSelected() && !scrollable && e.preventDefault(); - }; - _oldWheel: any; + } render() { TraceMobx(); return ( @@ -1889,7 +1708,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onClickScript={this.onChildClickHandler} onKey={this.onKeyDown} onDoubleClickScript={this.onChildDoubleClickHandler} - onBrowseClickScript={this.onBrowseClickHandler} childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} @@ -1912,47 +1730,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } } - -@observer -class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { - render() { - return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore - } -} - -export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { - const browseTransitionTime = 500; - SelectionManager.DeselectAll(); - dv && - DocumentManager.Instance.showDocument(dv.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { - if (!focused) { - const selfFfview = !dv.Document.isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - let containers = dv.containerViewPath?.() ?? []; - let parFfview = dv.CollectionFreeFormView; - for (var cont of containers) { - parFfview = parFfview ?? cont.CollectionFreeFormView; - } - while (parFfview?.Document.isGroup) parFfview = parFfview.DocumentView?.().CollectionFreeFormView; - const ffview = selfFfview && selfFfview.layoutDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview - ffview?.zoomSmoothlyAboutPt(ffview.screenToFreeformContentsXf.transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); - Doc.linkFollowHighlight(dv?.Document, false); - } - }); -} -ScriptingGlobals.add(CollectionBrowseClick); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { - !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); + !readOnly && (DocumentView.Selected()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { - !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); + !readOnly && (DocumentView.Selected()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) { - const selView = SelectionManager.Views; + const selView = DocumentView.Selected(); if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent'; runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.())); + return undefined; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function pinWithView(pinContent: boolean) { - SelectionManager.Views.forEach(view => + DocumentView.Selected().forEach(view => view._props.pinToPres(view.Document, { currentFrame: Cast(view.Document.currentFrame, 'number', null), pinData: { @@ -1963,29 +1758,33 @@ ScriptingGlobals.add(function pinWithView(pinContent: boolean) { }) ); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function bringToFront() { - SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document)); + DocumentView.Selected().forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document)); }); -ScriptingGlobals.add(function sendToBack(doc: Doc) { - SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true)); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function sendToBack() { + DocumentView.Selected().forEach(view => CollectionFreeFormView.from(view)?.bringToFront(view.Document, true)); }); -ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function datavizFromSchema() { // creating a dataviz doc to represent the schema table - SelectionManager.Views.forEach(view => { + DocumentView.Selected().forEach(viewIn => { + const view = viewIn; if (!view.layoutDoc.schema_columnKeys) { view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']); } - const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); + const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key !== 'text'); if (!keys) return; const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]); - let csvRows = []; + const csvRows = []; csvRows.push(keys.join(',')); for (let i = 0; i < children.length; i++) { - let eachRow = []; + const eachRow = []; for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]?.toString(); - if (cell) cell = cell.toString().replace(/\,/g, ''); + let cell = children[i][keys[j]]?.toString(); + if (cell) cell = cell.toString().replace(/,/g, ''); eachRow.push(cell); } csvRows.push(eachRow); |