diff options
Diffstat (limited to 'src/client/views/DocComponent.tsx')
-rw-r--r-- | src/client/views/DocComponent.tsx | 234 |
1 files changed, 140 insertions, 94 deletions
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 483b92957..b21b13e4c 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,161 +1,205 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; +import * as React from 'react'; +import { returnFalse } from '../../Utils'; import { DateField } from '../../fields/DateField'; -import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { Cast, DocCast, StrCast } from '../../fields/Types'; -import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; -import { returnFalse } from '../../Utils'; -import { DocUtils } from '../documents/Documents'; +import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util'; import { DocumentType } from '../documents/DocumentTypes'; -import { InteractionUtils } from '../util/InteractionUtils'; -import { DocumentView } from './nodes/DocumentView'; -import { Touchable } from './Touchable'; +import { DocUtils } from '../documents/Documents'; +import { DocumentManager } from '../util/DocumentManager'; +import { ObservableReactComponent } from './ObservableReactComponent'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; +import { FieldViewProps, FocusViewOptions } from './nodes/FieldView'; +import { DocumentView, OpenWhere } from './nodes/DocumentView'; +import { PinProps } from './nodes/trails'; +import { RefField } from '../../fields/RefField'; +import { DragManager } from '../util/DragManager'; -/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) +/** + * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc) + * Many of these methods only make sense for specific viewBox'es, but they should be written to + * be as general as possible + */ +export interface ViewBoxInterface { + fieldKey?: string; + annotationKey?: string; + updateIcon?: () => void; // updates the icon representation of the document + getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) + restoreView?: (viewSpec: Doc) => boolean; + scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus + brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms) + getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined + addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox + addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) + select?: (ctrlKey: boolean, shiftKey: boolean) => void; + focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>; + isAnyChildContentActive?: () => boolean; // is any child content of the document active + onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected + getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + playFrom?: (time: number, endTime?: number) => void; + Pause?: () => void; // pause a media document (eg, audio/video) + IsPlaying?: () => boolean; // is a media document playing + TogglePause?: (keep?: boolean) => void; // toggle media document playing state + setFocus?: () => void; // sets input focus to the componentView + setData?: (data: Field | Promise<RefField | undefined>) => boolean; + componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; + dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void; + dragConfig?: (dragData: DragManager.DocumentDragData) => void; + incrementalRendering?: () => void; + infoUI?: () => JSX.Element | null; + screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }>; + ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; + ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; + snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; + search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; +} +/** + * DocComponent returns a React base class used by Doc views with accessors for unpacking he Document,layoutDoc, and dataDoc's + * (note: this should not be used for the 'Box' views that render the contents of Doc views) + * Example derived views: CollectionFreeFormDocumentView, DocumentView, DocumentViewInternal) + * */ export interface DocComponentProps { Document: Doc; - fieldKey?: string; LayoutTemplate?: () => Opt<Doc>; LayoutTemplateString?: string; } export function DocComponent<P extends DocComponentProps>() { - class Component extends Touchable<P> { - //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then - @computed get Document() { - return this.props.Document; + class Component extends ObservableReactComponent<React.PropsWithChildren<P>> { + constructor(props: P) { + super(props); + makeObservable(this); } - // This is the "The Document" -- it encapsulates, data, layout, and any templates - @computed get rootDoc() { - return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; + + //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then + get Document() { + return this._props.Document; } // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info @computed get layoutDoc() { - return this.props.LayoutTemplateString ? this.props.Document : Doc.Layout(this.props.Document, this.props.LayoutTemplate?.()); + return this._props.LayoutTemplateString ? this.Document : Doc.Layout(this.Document, this._props.LayoutTemplate?.()); } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.Document[DocData] as Doc; + return this.Document[DocData]; } - // key where data is stored - @computed get fieldKey() { - return this.props.fieldKey; - } - - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; } -/// FieldViewBoxProps - a generic base class for field views that are not annotatable (e.g. InkingStroke, ColorBox) -interface ViewBoxBaseProps { - Document: Doc; - DataDoc?: Doc; - DocumentView?: () => DocumentView; - fieldKey: string; - isSelected: (outsideReaction?: boolean) => boolean; - isContentActive: () => boolean | undefined; - renderDepth: number; - rootSelected: (outsideReaction?: boolean) => boolean; -} -export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() { - class Component extends Touchable<P> { - //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then - //@computed get Document(): T { return schemaCtor(this.props.Document); } +/** + * base class for non-annotatable views that render the interior contents of a DocumentView. + * this unpacks the Document/layout/data docs as well as the fieldKey being rendered, + * and provides accessors for DocumentView and ScreenToLocalBoxXf + * Example views include: InkingStroke, FontIconBox, EquationBox, etc + */ +export function ViewBoxBaseComponent<P extends FieldViewProps>() { + class Component extends ObservableReactComponent<React.PropsWithChildren<P>> { + constructor(props: P) { + super(props); + makeObservable(this); + } + + ScreenToLocalBoxXf = () => this._props.ScreenToLocalTransform(); - // This is the "The Document" -- it encapsulates, data, layout, and any templates - @computed get rootDoc() { - return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; + get DocumentView() { + return this._props.DocumentView; + } + //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then + get Document() { + return this._props.Document; } // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info @computed get layoutDoc() { - return Doc.Layout(this.props.Document); + return Doc.Layout(this.Document); } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; + return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData]; } // key where data is stored - @computed get fieldKey() { - return this.props.fieldKey; + get fieldKey() { + return this._props.fieldKey; } - - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; } -/// DocAnnotatbleComponent -return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image) -export interface ViewBoxAnnotatableProps { - Document: Doc; - DataDoc?: Doc; - fieldKey: string; - filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) - isContentActive: () => boolean | undefined; - select: (isCtrlPressed: boolean) => void; - whenChildContentsActiveChanged: (isActive: boolean) => void; - isSelected: (outsideReaction?: boolean) => boolean; - rootSelected: (outsideReaction?: boolean) => boolean; - renderDepth: number; - isAnnotationOverlay?: boolean; -} -export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() { - class Component extends Touchable<P> { +/** + * base class for annotatable views that render the interior contents of a DocumentView + * This does what ViewBoxBaseComponent does and additionally provides accessor for the + * field key where annotations are stored as well as add/move/remove methods for handing + * annotations. + * This also provides methods to determine when the contents should be interactive + * (respond to pointerEvents) such as when the DocumentView container is selected or a + * peer child of the container is selected + * Example views include: PDFBox, ImageBox, MapBox, etc + */ +export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { + class Component extends ObservableReactComponent<React.PropsWithChildren<P>> { @observable _annotationKeySuffix = () => 'annotations'; @observable _isAnyChildContentActive = false; + + constructor(props: P) { + super(props); + makeObservable(this); + } + + ScreenToLocalBoxXf = () => this._props.ScreenToLocalTransform(); + + get DocumentView() { + return this._props.DocumentView; + } //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document() { - return this.props.Document; - } - // This is the "The Document" -- it encapsulates, data, layout, and any templates - @computed get rootDoc() { - return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; + return this._props.Document; } // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info @computed get layoutDoc() { - return Doc.Layout(this.props.Document); + return Doc.Layout(this.Document); } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; + return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData]; } // key where data is stored @computed get fieldKey() { - return this.props.fieldKey; + return this._props.fieldKey; } - - isAnyChildContentActive = () => this._isAnyChildContentActive; - - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - @computed public get annotationKey() { return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : ''); } @action.bound - removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean { + removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); const indocs = doc instanceof Doc ? [doc] : doc; const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin); - // docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true)); + // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true)); const targetDataDoc = this.dataDoc; const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = this.rootDoc !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; + const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - Doc.RemoveDocFromList(Doc.GetProto(doc), 'proto_embeddings', doc); + Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc); doc.embedContainer = undefined; - if (recent) { + if (recent && !dontAddToRemoved) { doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); } }); - this.isAnyChildContentActive() && this.props.select(false); + if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) { + (DocumentManager.Instance.getFirstDocumentView(targetDataDoc)?.ComponentView as CollectionFreeFormView)?.promoteCollection(); + } else { + this.isAnyChildContentActive() && this._props.select(false); + } return true; } @@ -167,22 +211,22 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() // moving it into the target. @action.bound moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => { - if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + if (Doc.AreProtosEqual(this._props.Document, targetCollection)) { return true; } const first = doc instanceof Doc ? doc : doc[0]; if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) { - return this.removeDocument(doc, annotationKey, false) && addDocument(doc, annotationKey); + return this.removeDocument(doc, annotationKey, false, true) && addDocument(doc, annotationKey); } return false; }; @action.bound addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => { const docs = doc instanceof Doc ? [doc] : doc; - if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { + if (this._props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.Document))) { return false; } - const targetDataDoc = this.rootDoc[DocData]; + const targetDataDoc = this.dataDoc; const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -193,9 +237,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) { added.forEach(doc => { doc._dragOnlyWithinContainer = undefined; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - else Doc.GetProto(doc).annotationOn = undefined; - Doc.SetContainer(doc, this.rootDoc); + if (annotationKey ?? this._annotationKeySuffix()) doc[DocData].annotationOn = this.Document; + else doc[DocData].annotationOn = undefined; + Doc.SetContainer(doc, this.Document); inheritParentAcls(targetDataDoc, doc, true); }); @@ -208,7 +252,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() return true; }; - whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + isAnyChildContentActive = () => this._isAnyChildContentActive; + + whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); } return Component; } |