aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/DocComponent.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/DocComponent.tsx')
-rw-r--r--src/client/views/DocComponent.tsx234
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;
}