aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DocumentView.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-01-23 16:11:42 -0500
committerbobzel <zzzman@gmail.com>2024-01-23 16:11:42 -0500
commitaf380979349308077e13fc12a2d09255b7f05f28 (patch)
tree79585221a23bccf2d352095b26bea99981ca92dc /src/client/views/nodes/DocumentView.tsx
parent001127c07f95173d7036db19d07dcfb1135f3caa (diff)
reorganization of DocumentView, DocumentViewInternal and FieldView methods and props. fix for selection bug after following a link. migrating to use [DocData] instad of GetProto()
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r--src/client/views/nodes/DocumentView.tsx1202
1 files changed, 491 insertions, 711 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 444c300f3..3e5e43b47 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,19 +3,17 @@ import { Dropdown, DropdownType, Type } from 'browndash-components';
import { Howl } from 'howler';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
import { Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
-import { AclPrivate, Animation, AudioPlay, DocViews } from '../../../fields/DocSymbols';
+import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
-import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { DocServer } from '../../DocServer';
@@ -33,34 +31,27 @@ import { SelectionManager } from '../../util/SelectionManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
-import { Transform } from '../../util/Transform';
-import { UndoManager, undoBatch } from '../../util/UndoManager';
+import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent } from '../DocComponent';
+import { DocComponent, ViewBoxInterface } from '../DocComponent';
import { EditableView } from '../EditableView';
import { GestureOverlay } from '../GestureOverlay';
import { LightboxView } from '../LightboxView';
-import { ObservableReactComponent } from '../ObservableReactComponent';
import { StyleProp } from '../StyleProvider';
-import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
-import { FieldViewProps } from './FieldView';
+import { FieldViewProps, FieldViewSharedProps } from './FieldView';
import { KeyValueBox } from './KeyValueBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PresEffect, PresEffectDirection } from './trails';
-import { PinProps, PresBox } from './trails/PresBox';
-
interface Window {
MediaRecorder: MediaRecorder;
}
-
declare class MediaRecorder {
- // whatever MediaRecorder has
- constructor(e: any);
+ constructor(e: any); // whatever MediaRecorder has
}
export enum OpenWhereMod {
@@ -92,114 +83,7 @@ export enum OpenWhere {
export function returnEmptyDocViewList() {
return [] as DocumentView[];
}
-export interface DocFocusOptions {
- willPan?: boolean; // determines whether to pan to target document
- willZoomCentered?: boolean; // determines whether to zoom in on target document. if zoomScale is 0, this just centers the document
- zoomScale?: number; // percent of containing frame to zoom into document
- zoomTime?: number;
- didMove?: boolean; // whether a document was changed during the showDocument process
- docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
- instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
- preview?: boolean; // whether changes should be previewed by the componentView or written to the document
- effect?: Doc; // animation effect for focus
- noSelect?: boolean; // whether target should be selected after focusing
- playAudio?: boolean; // whether to play audio annotation on focus
- playMedia?: boolean; // whether to play start target videos
- openLocation?: OpenWhere; // where to open a missing document
- zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
- toggleTarget?: boolean; // whether to toggle target on and off
- anchorDoc?: Doc; // doc containing anchor info to apply at end of focus to target doc
- easeFunc?: 'linear' | 'ease'; // transition method for scrolling
-}
-export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>;
-export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => any;
-export interface DocComponentView {
- 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: DocFocusOptions) => 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: DocFocusOptions) => 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: DocFocusOptions) => 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;
- 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;
-}
-/**
- * props that DocumentViews, DocumentInternalViews and FieldViews all can use
- * */
-export interface DocumentViewSharedProps {
- Document: Doc;
- LayoutTemplateString?: string;
- TemplateDataDocument?: Doc;
- renderDepth: number;
- scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
- treeViewDoc?: Doc;
- xPadding?: number;
- yPadding?: number;
- dontRegisterView?: boolean;
- dropAction?: dropActionType;
- dragAction?: dropActionType;
- forceAutoHeight?: boolean;
- ignoreAutoHeight?: boolean;
- disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
- CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
- containerViewPath?: () => DocumentView[];
- fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
- isGroupActive?: () => string | undefined; // is this document part of a group that is active
- setContentView?: (view: DocComponentView) => any;
- PanelWidth: () => number;
- PanelHeight: () => number;
- isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
- isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
- childFilters: () => string[];
- childFiltersByRanges: () => string[];
- styleProvider: Opt<StyleProviderFunc>;
- setTitleFocus?: () => void;
- focus: DocFocusFunc;
- layout_fitWidth?: (doc: Doc) => boolean | undefined;
- searchFilterDocs: () => Doc[];
- layout_showTitle?: () => string;
- whenChildContentsActiveChanged: (isActive: boolean) => void;
- rootSelected?: () => boolean; // whether the root of a template has been selected
- addDocTab: (doc: Doc, where: OpenWhere) => boolean;
- 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)
- addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
- removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
- moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => boolean;
- pinToPres: (document: Doc, pinProps: PinProps) => void;
- ScreenToLocalTransform: () => Transform;
- bringToFront: (doc: Doc, sendToBack?: boolean) => void;
- waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
- defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
- pointerEvents?: () => Opt<string>;
-}
-
-/**
- * props that are used by DocumentViews and DocumentInternalViews but not by the contents of those views (FieldViews).
- */
-export interface DocumentViewProps extends DocumentViewSharedProps {
+export interface DocumentViewProps extends FieldViewSharedProps {
hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
@@ -221,35 +105,22 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
NativeWidth?: () => number;
NativeHeight?: () => number;
- LayoutTemplate?: () => Opt<Doc>;
contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[];
- onClick?: () => ScriptField;
- onDoubleClick?: () => ScriptField;
- onPointerDown?: () => ScriptField;
- onPointerUp?: () => ScriptField;
- onBrowseClick?: () => ScriptField | undefined;
- onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
dragStarting?: () => void;
dragEnding?: () => void;
}
-
-/**
- * props used by DocInternalViews and FieldViews but not DocumentViews
- * these props correspond to things that the DocumentView creates and thus doesn't need to receive as a prop
- */
-export interface DocumentViewInternalSharedProps {
- select: (ctrlPressed: boolean, shiftPress?: boolean) => void;
- isSelected: () => boolean;
- docViewPath: () => DocumentView[];
- NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal
-}
-export interface DocumentViewInternalProps extends DocumentViewProps, DocumentViewInternalSharedProps {
- docViewPublic: () => DocumentView;
-}
-
@observer
-export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
+export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
+ // this makes mobx trace() statements more descriptive
+ public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
+
+ /**
+ * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without
+ * needing to know about/reference MainView
+ */
+ public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
+
private _disposers: { [name: string]: IReactionDisposer } = {};
private _doubleClickTimeout: NodeJS.Timeout | undefined;
private _singleClickFunc: undefined | (() => any);
@@ -262,79 +133,51 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
private _dropDisposer?: DragManager.DragDropDisposer;
- constructor(props: any) {
+ constructor(props: FieldViewProps & DocumentViewProps) {
super(props);
makeObservable(this);
}
- @observable _componentView: Opt<DocComponentView> = undefined; // needs to be accessed from DocumentView wrapper class
+ @observable _changingTitleField = false;
+ @observable _titleDropDownInnerWidth = 0; // width of menu dropdown when setting doc title
+ @observable _mounted = false; // turn off all pointer events if component isn't yet mounted (enables nested Docs in alternate UI textboxes that appear on hover which otherwise would grab focus from the text box, reverting to the original UI )
+ @observable _isContentActive: boolean | undefined = undefined;
+ @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined;
+ @observable _componentView: Opt<ViewBoxInterface> = undefined; // needs to be accessed from DocumentView wrapper class
@observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views. defaults to 300 if not uset
@observable _animateScalingTo = 0;
- public get animateScaleTime() {
- return this._animateScaleTime ?? 100;
- }
- public get displayName() {
- return 'DocumentViewInternal(' + this.Document.title + ')';
- } // this makes mobx trace() statements more descriptive
+ get _contentDiv() { return this._mainCont.current; } // prettier-ignore
+ get _docView() { return this._props.DocumentView?.(); } // prettier-ignore
+
+ animateScaleTime = () => this._animateScaleTime ?? 100;
+ style = (doc: Doc, sprop: StyleProp | string) => this._props.styleProvider?.(doc, this._props, sprop);
+ @computed get layout_showTitle() { return this.style(this.layoutDoc, StyleProp.ShowTitle) as Opt<string>; } // prettier-ignore
+ @computed get opacity() { return this.style(this.layoutDoc, StyleProp.Opacity); } // prettier-ignore
+ @computed get boxShadow() { return this.style(this.layoutDoc, StyleProp.BoxShadow); } // prettier-ignore
+ @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding); } // prettier-ignore
+ @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations); } // prettier-ignore
+ @computed get backgroundBoxColor() { return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':box'); } // prettier-ignore
+ @computed get headerMargin() { return this.style(this.layoutDoc, StyleProp.HeaderMargin) ?? 0; } // prettier-ignore
+ @computed get layout_showCaption() { return this.style(this.layoutDoc, StyleProp.ShowCaption) ?? 0; } // prettier-ignore
+ @computed get titleHeight() { return this.style(this.layoutDoc, StyleProp.TitleHeight) ?? 0; } // prettier-ignore
+ @computed get docContents() { return this.style(this.Document, StyleProp.DocContents); } // prettier-ignore
+ @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore
+ @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore
- public get DocumentView() {
- return this._props.docViewPublic;
- }
-
- public get ContentDiv() {
- return this._mainCont.current;
- }
- public get LayoutFieldKey() {
- return Doc.LayoutFieldKey(this.layoutDoc);
- }
- @computed get styleProps(): FieldViewProps {
- return { ...this._props, fieldKey: '' };
- }
- @computed get layout_showTitle() {
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.ShowTitle) as Opt<string>;
- }
- @computed get NativeDimScaling() {
- return this._props.NativeDimScaling?.() || 1;
- }
- @computed get thumb() {
- return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
- }
- @computed get opacity() {
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.Opacity);
- }
- @computed get boxShadow() {
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.BoxShadow);
- }
- @computed get borderRounding() {
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.BorderRounding);
- }
- @computed get widgetDecorations() {
- TraceMobx();
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.Decorations);
- }
- @computed get backgroundBoxColor() {
- return this._props.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.BackgroundColor + ':box');
- }
- @computed get docContents() {
- return this._props.styleProvider?.(this.Document, this.styleProps, StyleProp.DocContents);
- }
- @computed get headerMargin() {
- return this._props?.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.HeaderMargin) || 0;
- }
- @computed get layout_showCaption() {
- return this._props?.hideCaptions ? undefined : this._props?.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.ShowCaption) || 0;
+ @computed get onClickHandler() {
+ return this._props.onClickScript?.() ?? this._props.onBrowseClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick));
}
- @computed get titleHeight() {
- return this._props?.styleProvider?.(this.layoutDoc, this.styleProps, StyleProp.TitleHeight) || 0;
+ @computed get onDoubleClickHandler() {
+ return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick, ScriptCast(this.Document.onDoubleClick));
}
- @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined;
- @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
- return this._pointerEvents;
+ @computed get onPointerDownHandler() {
+ return this._props.onPointerDownScript?.() ?? ScriptCast(this.layoutDoc.onPointerDown, ScriptCast(this.Document.onPointerDown));
}
- @computed get finalLayoutKey() {
- return StrCast(this.Document.layout_fieldKey, 'layout');
+ @computed get onPointerUpHandler() {
+ return this._props.onPointerUpScript?.() ?? ScriptCast(this.layoutDoc.onPointerUp, ScriptCast(this.Document.onPointerUp));
}
+
@computed get disableClickScriptFunc() {
const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
// prettier-ignore
@@ -344,23 +187,50 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
(onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.()))
);
}
- @computed get onClickHandler() {
- return this._props.onClick?.() ?? this._props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
+ @computed get _rootSelected() {
+ return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.());
}
- @computed get onDoubleClickHandler() {
- return this._props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick;
+ /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
+ @computed get _contentPointerEvents() {
+ TraceMobx();
+ return this._props.contentPointerEvents ??
+ ((!this.disableClickScriptFunc && //
+ this.onClickHandler &&
+ !this._props.onBrowseClickScript?.() &&
+ this.isContentActive() !== true) ||
+ this.isContentActive() === false)
+ ? 'none'
+ : this._pointerEvents;
}
- @computed get onPointerDownHandler() {
- return this._props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown);
+
+ // We need to use allrelatedLinks to get not just links to the document as a whole, but links to
+ // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g.,
+ // - PDF text regions are rendered as an Annotations without generating a DocumentView, '
+ // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
+ // - and links to PDF/Web docs at a certain scroll location never create an explicit view.
+ // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
+ @computed get directLinks() {
+ TraceMobx();
+ return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(
+ link =>
+ (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) ||
+ (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) ||
+ ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.Document)) ||
+ ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document))
+ );
}
- @computed get onPointerUpHandler() {
- return this._props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp);
+ @computed get _allLinks() {
+ TraceMobx();
+ return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
+ }
+
+ @computed get filteredLinks() {
+ return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
}
componentWillUnmount() {
this.cleanupHandlers(true);
}
- @observable _mounted = false; // turn off all pointer events if component isn't yet mounted (enables nested Docs in alternate UI textboxes that appear on hover which otherwise would grab focus from the text box, reverting to the original UI )
componentDidMount() {
runInAction(() => (this._mounted = true));
@@ -380,7 +250,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
{ fireImmediately: true }
);
this._disposers.pointerevents = reaction(
- () => this._props.styleProvider?.(this.Document, this.styleProps, StyleProp.PointerEvents),
+ () => this.style(this.Document, StyleProp.PointerEvents),
pointerevents => (this._pointerEvents = pointerevents),
{ fireImmediately: true }
);
@@ -406,18 +276,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
- if (this._mainCont.current) {
+ const docView = this._docView;
+ if (this._mainCont.current && docView) {
const views = SelectionManager.Views.filter(dv => dv.ContentDiv);
- const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [this.DocumentView()];
+ const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [docView];
const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document));
- const screenXf = this.DocumentView().screenToViewTransform();
+ const screenXf = docView.screenToViewTransform();
const [left, top] = screenXf.inverse().transformPoint(0, 0);
dragData.offset = screenXf.transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.treeViewDoc = this._props.treeViewDoc;
dragData.removeDocument = this._props.removeDocument;
dragData.moveDocument = this._props.moveDocument;
- dragData.draggedViews = [this.DocumentView()];
+ dragData.draggedViews = [docView];
dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false;
DragManager.StartDocumentDrag(
selected.map(dv => dv.ContentDiv!),
@@ -434,12 +305,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title';
setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
};
-
- public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
-
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
- if (!this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+ const documentView = this._docView;
+ if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
!this.layoutDoc._keepZWhenDragged && this._props.bringToFront(this.Document);
@@ -451,7 +320,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const func = () => this.onDoubleClickHandler.script.run( {
this: this.Document,
scriptContext: this._props.scriptContext,
- documentView: this.DocumentView(),
+ documentView,
clientX, clientY, altKey, shiftKey, ctrlKey,
value: undefined,
}, console.log );
@@ -482,7 +351,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
this: this.Document,
_readOnly_: false,
scriptContext: this._props.scriptContext,
- documentView: this.DocumentView(),
+ documentView,
clientX,
clientY,
shiftKey,
@@ -506,7 +375,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const sendToBack = e.altKey;
this._singleClickFunc =
// prettier-ignore
- clickFunc ?? (() => (sendToBack ? this.DocumentView()._props.bringToFront(this.Document, true) :
+ clickFunc ?? (() => (sendToBack ? documentView._props.bringToFront(this.Document, true) :
this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ??
this._props.select(e.ctrlKey||e.shiftKey, e.metaKey)));
const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
@@ -525,12 +394,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onPointerDown = (e: React.PointerEvent): void => {
if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
- this._longPressSelector = setTimeout(() => {
- if (DocumentView.LongPress) {
- this._props.select(false);
- }
- }, 1000);
- if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.DocumentView();
+ this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000);
+ if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView;
this._downX = e.clientX;
this._downY = e.clientY;
@@ -541,7 +406,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (
// prettier-ignore
(this._props.isDocumentActive?.() || this._props.isContentActive?.()) &&
- !this._props.onBrowseClick?.() &&
+ !this._props.onBrowseClickScript?.() &&
!this.Document.ignoreClick &&
e.button === 0 &&
!Doc.IsInMyOverlay(this.layoutDoc)
@@ -587,38 +452,39 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (DocumentView.LongPress) e.preventDefault();
};
- @undoBatch
- toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
+ toggleFollowLink = undoable((zoom?: boolean, setTargetToggle?: boolean): void => {
const hadOnClick = this.Document.onClick;
this.noOnClick();
this.Document.onClick = hadOnClick ? undefined : FollowLinkScript();
this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never';
- };
- @undoBatch
- followLinkOnClick = () => {
+ }, 'toggle follow link');
+
+ followLinkOnClick = undoable(() => {
this.Document.ignoreClick = false;
this.Document.onClick = FollowLinkScript();
this.Document.followLinkToggle = false;
this.Document.followLinkZoom = false;
this.Document.followLinkLocation = undefined;
- };
- @undoBatch
- noOnClick = () => {
- this.Document.ignoreClick = false;
- this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined;
- };
+ }, 'follow link on click');
- @undoBatch deleteClicked = () => this._props.removeDocument?.(this.Document);
- @undoBatch setToggleDetail = () =>
- (this.Document.onClick = ScriptField.MakeScript(
- `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
- .replace('layout_', '')
- .replace(/^layout$/, 'detail')}")`,
- { documentView: 'any' }
- ));
-
- @undoBatch
- drop = (e: Event, de: DragManager.DropEvent) => {
+ noOnClick = undoable(() => {
+ this.Document.ignoreClick = false;
+ this.Document.onClick = this.Document[DocData].onClick = undefined;
+ }, 'default on click');
+
+ deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
+ setToggleDetail = undoable(
+ () =>
+ (this.Document.onClick = ScriptField.MakeScript(
+ `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
+ .replace('layout_', '')
+ .replace(/^layout$/, 'detail')}")`,
+ { documentView: 'any' }
+ )),
+ 'set toggle detail'
+ );
+
+ drop = undoable((e: Event, de: DragManager.DropEvent) => {
if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false;
if (this.Document === Doc.ActiveDashboard) {
e.stopPropagation();
@@ -642,7 +508,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
if (de.complete.linkDocument) {
de.complete.linkDocument.layout_isSvg = true;
- this.DocumentView().CollectionFreeFormView?.addDocument(de.complete.linkDocument);
+ this._docView?.CollectionFreeFormView?.addDocument(de.complete.linkDocument);
}
}
e.stopPropagation();
@@ -650,11 +516,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
}
return false;
- };
+ }, 'drop doc');
- @undoBatch
- makeIntoPortal = () => {
- const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.Document && d.link_relationship === 'portal to:portal from');
+ makeIntoPortal = undoable(() => {
+ const portalLink = this._allLinks.find(d => d.link_anchor_1 === this.Document && d.link_relationship === 'portal to:portal from');
if (!portalLink) {
DocUtils.MakeLink(
this.Document,
@@ -670,7 +535,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
this.Document.followLinkLocation = OpenWhere.lightbox;
this.Document.onClick = FollowLinkScript();
- };
+ }, 'make into portal');
importDocument = () => {
const input = document.createElement('input');
@@ -716,7 +581,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (e && !(e.nativeEvent as any).dash) {
const onDisplay = () => {
- if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this._props.isSelected() && SelectionManager.SelectView(this.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && this._props.select(false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY));
};
if (navigator.userAgent.includes('Macintosh')) {
@@ -811,7 +676,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const constantItems: ContextMenuProps[] = [];
if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) {
constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.Document) });
- (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.DocumentView()), icon: 'users' });
+ (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' });
if (this._props.removeDocument && Doc.ActiveDashboard !== this.Document) {
// need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
@@ -877,63 +742,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, undefined);
};
- @computed get _rootSelected() {
- return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.());
- }
rootSelected = () => this._rootSelected;
panelHeight = () => this._props.PanelHeight() - this.headerMargin;
screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
- onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
+ onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler;
setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = height);
- setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- @observable _isContentActive: boolean | undefined = undefined;
-
+ setContentView = action((view: ViewBoxInterface) => (this._componentView = view));
isContentActive = (): boolean | undefined => this._isContentActive;
childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
- /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
- @computed get _contentPointerEvents() {
- TraceMobx();
- return this._props.contentPointerEvents ??
- ((!this.disableClickScriptFunc && //
- this.onClickHandler &&
- !this._props.onBrowseClick?.() &&
- this.isContentActive() !== true) ||
- this.isContentActive() === false)
- ? 'none'
- : this.pointerEvents;
- }
contentPointerEvents = () => this._contentPointerEvents;
- @computed get contents() {
- TraceMobx();
- const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
- const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
- return (
- <div
- className="documentView-contentsView"
- style={{
- pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
- height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
- }}>
- <DocumentContentsView
- key={1}
- {...this.styleProps}
- fieldKey={this.finalLayoutKey}
- pointerEvents={this.contentPointerEvents}
- setContentView={this.setContentView}
- childFilters={this.childFilters}
- PanelHeight={this.panelHeight}
- setHeight={this.setHeight}
- isContentActive={this.isContentActive}
- ScreenToLocalTransform={this.screenToLocalContent}
- rootSelected={this.rootSelected}
- onClick={this.onClickFunc}
- setTitleFocus={this.setTitleFocus}
- />
- {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
- </div>
- );
- }
anchorPanelWidth = () => this._props.PanelWidth() || 1;
anchorPanelHeight = () => this._props.PanelHeight() || 1;
@@ -950,33 +768,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
return this._props.styleProvider?.(doc, props, property);
};
- // We need to use allrelatedLinks to get not just links to the document as a whole, but links to
- // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g.,
- // - PDF text regions are rendered as an Annotations without generating a DocumentView, '
- // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
- // - and links to PDF/Web docs at a certain scroll location never create an explicit view.
- // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
- @computed get directLinks() {
- TraceMobx();
- return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(
- link =>
- (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) ||
- (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) ||
- ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.Document)) ||
- ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document))
- );
- }
- @computed get allLinks() {
- TraceMobx();
- return LinkManager.Instance.getAllRelatedLinks(this.Document);
- }
- hideLink = computedFn((link: Doc) => () => (link.link_displayLine = false));
- @computed get allLinkEndpoints() {
+
+ removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false);
+ allLinkEndpoints = () => {
// the small blue dots that mark the endpoints of links
- TraceMobx();
if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
- const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
- return filtered.map(link => (
+ return this.filteredLinks.map(link => (
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView
{...this._props}
@@ -990,107 +787,54 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
hideCaptions={true}
hideLinkAnchors={true}
layout_fitWidth={returnTrue}
- removeDocument={this.hideLink(link)}
+ removeDocument={this.removeLinkByHiding(link)}
styleProvider={this.anchorStyleProvider}
LayoutTemplate={undefined}
LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)}
/>
</div>
));
- }
-
- static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
- let gumStream: any;
- let recorder: any;
- navigator.mediaDevices
- .getUserMedia({
- audio: true,
- })
- .then(function (stream) {
- let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
- if (audioTextAnnos) audioTextAnnos.push('');
- else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
- DictationManager.Controls.listen({
- interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
- continuous: { indefinite: false },
- }).then(results => {
- if (results && [DictationManager.Controls.Infringed].includes(results)) {
- DictationManager.Controls.stop();
- }
- onEnd?.();
- });
+ };
- gumStream = stream;
- recorder = new MediaRecorder(stream);
- recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
- if (!(result instanceof Error)) {
- const audioField = new AudioField(result.accessPaths.agnostic.client);
- const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
- if (audioAnnos === undefined) {
- dataDoc[field + '_audioAnnotations'] = new List([audioField]);
- } else {
- audioAnnos.push(audioField);
- }
- }
- };
- //runInAction(() => (dataDoc.audioAnnoState = 'recording'));
- recorder.start();
- const stopFunc = () => {
- recorder.stop();
- DictationManager.Controls.stop(false);
- runInAction(() => (dataDoc.audioAnnoState = 'stopped'));
- gumStream.getAudioTracks()[0].stop();
- };
- if (onRecording) onRecording(stopFunc);
- else setTimeout(stopFunc, 5000);
- });
- }
- playAnnotation = () => {
- const self = this;
- const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
- const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null);
- const anno = audioAnnos?.lastElement();
- if (anno instanceof AudioField) {
- switch (audioAnnoState) {
- case 'stopped':
- this.dataDoc[AudioPlay] = new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
- });
- this.dataDoc.audioAnnoState = 'playing';
- break;
- case 'playing':
- this.dataDoc[AudioPlay]?.stop();
- this.dataDoc.audioAnnoState = 'stopped';
- break;
- }
- }
+ viewBoxContents = () => {
+ TraceMobx();
+ const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
+ const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+ return (
+ <div
+ className="documentView-contentsView"
+ style={{
+ pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
+ height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+ }}>
+ <DocumentContentsView
+ {...this._props}
+ layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
+ pointerEvents={this.contentPointerEvents}
+ setContentViewBox={this.setContentView}
+ childFilters={this.childFilters}
+ PanelHeight={this.panelHeight}
+ setHeight={this.setHeight}
+ isContentActive={this.isContentActive}
+ ScreenToLocalTransform={this.screenToLocalContent}
+ rootSelected={this.rootSelected}
+ onClickScript={this.onClickFunc}
+ setTitleFocus={this.setTitleFocus}
+ />
+ {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()}
+ </div>
+ );
};
captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
- @observable _changingTitleField = false;
- @observable _dropDownInnerWidth = 0;
- fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => {
- const filteredOptions = new Set(inputOptions);
- const scaling = this.titleHeight / 30; /* height of Dropdown */
- Object.entries(DocOptions)
- .filter(opts => opts[1].filterable)
- .forEach((pair: [string, FInfo]) => filteredOptions.add(pair[0]));
- filteredOptions.add(StrCast(this.layoutDoc.layout_showTitle));
- const options = Array.from(filteredOptions)
- .filter(f => f)
- .map(facet => ({ val: facet, text: facet }));
+ fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => {
+ const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields));
return (
<div style={{ width: dropdownWidth }}>
<div
- ref={action((r: any) => r && (this._dropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))}
+ ref={action((r: any) => r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))}
onPointerDown={action(e => (this._changingTitleField = true))}
- style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${scaling})` }}>
+ style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}>
<Dropdown
activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))}
selectedVal={placeholder}
@@ -1100,7 +844,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
type={Type.TERT}
closeOnSelect={true}
dropdownType={DropdownType.SELECT}
- items={options}
+ items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))}
width={100}
fillWidth
/>
@@ -1108,43 +852,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
};
- @computed get innards() {
- TraceMobx();
+ /**
+ * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by
+ * setting layout_showTitle using the format: field1[;field2[...][:hover]]
+ * from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover]
+ **/
+ titleView = () => {
const showTitle = this.layout_showTitle?.split(':')[0];
const showTitleHover = this.layout_showTitle?.includes(':hover');
- const captionView = !this.layout_showCaption ? null : (
- <div
- className="documentView-captionWrapper"
- style={{
- pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined,
- background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'),
- color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')),
- }}>
- <FormattedTextBox
- {...this._props}
- yPadding={10}
- xPadding={10}
- fieldKey={this.layout_showCaption}
- styleProvider={this.captionStyleProvider}
- dontRegisterView={true}
- noSidebar={true}
- dontScale={true}
- renderDepth={this._props.renderDepth}
- isContentActive={this.isContentActive}
- />
- </div>
- );
+
const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.Document;
const background = StrCast(
this.layoutDoc.layout_headingColor,
StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor))
);
- const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._dropDownInnerWidth * this.titleHeight) / 30) : 0;
+ const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0;
const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
- // displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by
- // setting layout_showTitle using the format: field1[;field2[...][:hover]]
- // from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover]
- const titleView = !showTitle ? null : (
+
+ return !showTitle ? null : (
<div
className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
key="title"
@@ -1160,7 +885,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
{!dropdownWidth
? null
: this.fieldsDropdown(
- [],
+ [StrCast(this.layoutDoc.layout_showTitle)],
dropdownWidth,
StrCast(this.layoutDoc.layout_showTitle).split(':')[0],
action((field: string | number) => {
@@ -1206,19 +931,36 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
</div>
);
- return this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
- this.contents
- ) : (
- <div className="documentView-styleWrapper">
- {titleView}
- {this.contents}
- {captionView}
+ };
+
+ captionView = () => {
+ return !this.layout_showCaption ? null : (
+ <div
+ className="documentView-captionWrapper"
+ style={{
+ pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined,
+ background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'),
+ color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')),
+ }}>
+ <FormattedTextBox
+ {...this._props}
+ yPadding={10}
+ xPadding={10}
+ fieldKey={this.layout_showCaption}
+ styleProvider={this.captionStyleProvider}
+ dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
+ renderDepth={this._props.renderDepth}
+ isContentActive={this.isContentActive}
+ />
</div>
);
- }
+ };
renderDoc = (style: object) => {
TraceMobx();
+ const showTitle = this.layout_showTitle?.split(':')[0];
return !DocCast(this.Document) || GetEffectiveAcl(this.dataDoc) === AclPrivate
? null
: this.docContents ?? (
@@ -1234,49 +976,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
fontSize: Cast(this.Document._text_fontSize, 'string', null),
transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+ transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
}}>
- {this.innards}
+ {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
+ this.viewBoxContents()
+ ) : (
+ <div className="documentView-styleWrapper">
+ {this.titleView()}
+ {this.viewBoxContents()}
+ {this.captionView()}
+ </div>
+ )}
{this.widgetDecorations ?? null}
</div>
);
};
- /**
- * returns an entrance animation effect function to wrap a JSX element
- * @param presEffectDoc presentation effects document that specifies the animation effect parameters
- * @returns a function that will wrap a JSX animation element wrapping any JSX element
- */
- public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
- const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection;
- const effectProps = {
- left: dir === PresEffectDirection.Left,
- right: dir === PresEffectDirection.Right,
- top: dir === PresEffectDirection.Top,
- bottom: dir === PresEffectDirection.Bottom,
- opposite: true,
- delay: 0,
- duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
- };
- //prettier-ignore
- switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
- default:
- case PresEffect.None: return renderDoc;
- case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
- case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>;
- case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>;
- case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>;
- case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>;
- case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>;
- case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>;
- }
- }
- @computed get highlighting() {
- return this._props.styleProvider?.(this.Document, { ...this._props, fieldKey: '' }, StyleProp.Highlighting);
- }
- @computed get borderPath() {
- return this._props.styleProvider?.(this.Document, { ...this._props, fieldKey: '' }, StyleProp.BorderPath);
- }
render() {
TraceMobx();
const highlighting = this.highlighting;
@@ -1304,10 +1019,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onClick={this.onClick}
onPointerEnter={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
onPointerOver={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
- onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)}
+ onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)}
style={{
borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
+ pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
}}>
<>
{this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)}
@@ -1316,176 +1031,124 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
}
-}
-
-@observer
-export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
- public static ROOT_DIV = 'documentView-effectsWrapper';
- constructor(props: any) {
- super(props);
- makeObservable(this);
+ /**
+ * returns an entrance animation effect function to wrap a JSX element
+ * @param presEffectDoc presentation effects document that specifies the animation effect parameters
+ * @returns a function that will wrap a JSX animation element wrapping any JSX element
+ */
+ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
+ const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection;
+ const effectProps = {
+ left: dir === PresEffectDirection.Left,
+ right: dir === PresEffectDirection.Right,
+ top: dir === PresEffectDirection.Top,
+ bottom: dir === PresEffectDirection.Bottom,
+ opposite: true,
+ delay: 0,
+ duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
+ };
+ //prettier-ignore
+ switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
+ default:
+ case PresEffect.None: return renderDoc;
+ case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
+ case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>;
+ case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>;
+ case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>;
+ case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>;
+ case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>;
+ case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>;
+ }
}
+ public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
+ let gumStream: any;
+ let recorder: any;
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ })
+ .then(function (stream) {
+ let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
+ if (audioTextAnnos) audioTextAnnos.push('');
+ else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
+ DictationManager.Controls.listen({
+ interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ onEnd?.();
+ });
- @observable _selected = false;
- public get IsSelected() {
- return this._selected;
- }
- public set IsSelected(val) {
- runInAction(() => (this._selected = val));
- }
- @observable public static LongPress = false;
- @computed public static get exploreMode() {
- return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
+ gumStream = stream;
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
+ if (!(result instanceof Error)) {
+ const audioField = new AudioField(result.accessPaths.agnostic.client);
+ const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
+ if (audioAnnos === undefined) {
+ dataDoc[field + '_audioAnnotations'] = new List([audioField]);
+ } else {
+ audioAnnos.push(audioField);
+ }
+ }
+ };
+ //runInAction(() => (dataDoc.audioAnnoState = 'recording'));
+ recorder.start();
+ const stopFunc = () => {
+ recorder.stop();
+ DictationManager.Controls.stop(false);
+ runInAction(() => (dataDoc.audioAnnoState = 'stopped'));
+ gumStream.getAudioTracks()[0].stop();
+ };
+ if (onRecording) onRecording(stopFunc);
+ else setTimeout(stopFunc, 5000);
+ });
}
- @observable private _docViewInternal: DocumentViewInternal | undefined | null = undefined;
- @observable private _htmlOverlayText: Opt<string> = undefined;
- @observable private _isHovering = false;
- private _htmlOverlayEffect: Opt<Doc>;
+}
- public get displayName() {
- return 'DocumentView(' + this.Document?.title + ')';
- } // this makes mobx trace() statements more descriptive
+@observer
+export class DocumentView extends DocComponent<DocumentViewProps>() {
+ public static ROOT_DIV = 'documentView-effectsWrapper';
+ public get displayName() { return 'DocumentView(' + this.Document?.title + ')'; } // prettier-ignore
public ContentRef = React.createRef<HTMLDivElement>();
- public ViewTimer: NodeJS.Timeout | undefined; // timer for res
- public AnimEffectTimer: NodeJS.Timeout | undefined; // timer for res
+ private _htmlOverlayEffect: Opt<Doc>;
private _disposers: { [name: string]: IReactionDisposer } = {};
- public clearViewTransition = () => {
- this.ViewTimer && clearTimeout(this.ViewTimer);
- this.layoutDoc._viewTransition = undefined;
- };
- playAnnotation = () => this._docViewInternal?.playAnnotation();
- noOnClick = () => this._docViewInternal?.noOnClick();
- makeIntoPortal = () => this._docViewInternal?.makeIntoPortal();
- setToggleDetail = () => this._docViewInternal?.setToggleDetail();
- onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
- cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
- public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
+ private _viewTimer: NodeJS.Timeout | undefined;
+ private _animEffectTimer: NodeJS.Timeout | undefined;
- public showContextMenu = (pageX: number, pageY: number) => this._docViewInternal?.onContextMenu(undefined, pageX, pageY);
-
- public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
- this._htmlOverlayText = text;
- this._htmlOverlayEffect = effect;
- });
- public setAnimateScaling = action((scale: number, time?: number) => {
- if (this._docViewInternal) {
- this._docViewInternal._animateScalingTo = scale;
- this._docViewInternal._animateScaleTime = time;
- }
- });
- public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
- this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer);
- this.Document[Animation] = presEffect;
- this.AnimEffectTimer = setTimeout(() => (this.Document[Animation] = undefined), timeInMs);
- };
- public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
- this.layoutDoc._viewTransition = `${transProp} ${timeInMs}ms`;
- if (dataTrans) this.Document._dataTransition = `${transProp} ${timeInMs}ms`;
- this.ViewTimer && clearTimeout(this.ViewTimer);
- return (this.ViewTimer = setTimeout(() => {
- this.layoutDoc._viewTransition = undefined;
- this.Document._dataTransition = 'inherit';
- afterTrans?.();
- }, timeInMs + 10));
- };
- public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) {
- docs.forEach(doc => {
- doc._viewTransition = `${transProp} ${timeInMs}ms`;
- dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
- });
- return setTimeout(
- () =>
- docs.forEach(doc => {
- doc._viewTransition = undefined;
- dataTrans && (doc.dataTransition = 'inherit');
- afterTrans?.();
- }),
- timeInMs + 10
- );
+ @computed public static get exploreMode() {
+ return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
}
- // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
- public static showBackLinks(linkAnchor: Doc) {
- const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
- // prettier-ignore
- DocServer.GetRefField(docId).then(docx =>
- LightboxView.Instance.SetLightboxDoc(
- (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection
- Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, onViewMounted: ScriptField.MakeScript('updateLinkCollection(this, this.target)') }, docId)
- )
- );
+ constructor(props: DocumentViewProps) {
+ super(props);
+ makeObservable(this);
}
- toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
- get Document() {
- return this._props.Document;
- }
- get topMost() {
- return this._props.renderDepth === 0;
- }
- get dataDoc() {
- return this._docViewInternal?.dataDoc ?? this.Document;
- }
- get ContentDiv() {
- return this._docViewInternal?.ContentDiv;
- }
- get ComponentView() {
- return this._docViewInternal?._componentView;
- }
- get allLinks() {
- return (this._docViewInternal?.allLinks || []).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
- }
- get LayoutFieldKey() {
- return this._docViewInternal?.LayoutFieldKey || 'layout';
- }
- @computed get layout_fitWidth() {
- return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
- }
- @computed get anchorViewDoc() {
- return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined;
- }
- @computed get hideLinkButton() {
- return (
- this._props.hideLinkButton ||
- this._props.renderDepth === -1 || //
- (this.IsSelected && this._props.renderDepth) ||
- !this._isHovering ||
- (!this.IsSelected && this.layoutDoc.layout_hideLinkButton) ||
- SnappingManager.IsDragging ||
- SnappingManager.IsResizing
- );
- }
- hideLinkCount = () => (this.hideLinkButton ? true : false);
+ // want the htmloverlay to be able to fade in but we also want it to be display 'none' until it is needed.
+ // unfortunately, CSS can't transition animate any properties for something that is display 'none'.
+ // so we need to first activate the div, then, after a render timeout, start the opacity transition.
+ @observable private _enableHtmlOverlayTransitions: boolean = false;
+ @observable private _docViewInternal: DocumentViewInternal | undefined | null = undefined;
+ @observable private _htmlOverlayText: Opt<string> = undefined;
+ @observable private _isHovering = false;
+ @observable private _selected = false;
+ @observable public static LongPress = false;
- @computed get linkCountView() {
- return <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
- }
- /**
- * path of DocumentViews hat contains this DocumentView (does not includes this DocumentView thouhg)
- */
- @computed get containerViewPath() {
- return this._props.containerViewPath;
- }
- @computed get layoutDoc() {
- return Doc.Layout(this.Document, this._props.LayoutTemplate?.());
- }
- @computed get nativeWidth() {
- return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
- }
- @computed get nativeHeight() {
- return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
- }
- @computed get shouldNotScale() {
+ @computed private get shouldNotScale() {
return (this.layout_fitWidth && !this.nativeWidth) || this._props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any);
}
- @computed get effectiveNativeWidth() {
+ @computed private get effectiveNativeWidth() {
return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
}
- @computed get effectiveNativeHeight() {
+ @computed private get effectiveNativeHeight() {
return this.shouldNotScale ? 0 : this.nativeHeight || NumCast(this.layoutDoc.height);
}
- @computed get nativeScaling() {
+ @computed private get nativeScaling() {
if (this.shouldNotScale) return 1;
const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
@@ -1493,19 +1156,19 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
}
return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
- @computed get panelWidth() {
+ @computed private get panelWidth() {
return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
}
- @computed get panelHeight() {
+ @computed private get panelHeight() {
if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.layout_reflowVertical)) {
return Math.min(this._props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
}
return this._props.PanelHeight();
}
- @computed get Xshift() {
+ @computed private get Xshift() {
return this.effectiveNativeWidth ? Math.max(0, (this._props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0;
}
- @computed get Yshift() {
+ @computed private get Yshift() {
return this.effectiveNativeWidth &&
this.effectiveNativeHeight &&
Math.abs(this.Xshift) < 0.001 &&
@@ -1513,41 +1176,99 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
? Math.max(0, (this._props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2)
: 0;
}
- @computed get centeringX() {
- return this._props.dontCenter?.includes('x') ? 0 : this.Xshift;
+ @computed private get hideLinkButton() {
+ return (
+ this._props.hideLinkButton ||
+ this._props.renderDepth === -1 || //
+ (this.IsSelected && this._props.renderDepth) ||
+ !this._isHovering ||
+ (!this.IsSelected && this.layoutDoc.layout_hideLinkButton) ||
+ SnappingManager.IsDragging ||
+ SnappingManager.IsResizing
+ );
}
- @computed get centeringY() {
- return this._props.dontCenter?.includes('y') ? 0 : this.Yshift;
+
+ componentDidMount() {
+ runInAction(() => this.Document[DocViews].add(this));
+ this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction);
+ !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this);
}
- @computed get CollectionFreeFormView() {
- return this.CollectionFreeFormDocumentView?.CollectionFreeFormView;
+ componentWillUnmount() {
+ runInAction(() => this.Document[DocViews].delete(this));
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
}
- @computed get CollectionFreeFormDocumentView() {
- return this._props.CollectionFreeFormDocumentView?.();
+
+ public set IsSelected(val) { runInAction(() => (this._selected = val)); } // prettier-ignore
+ public get IsSelected() { return this._selected; } // prettier-ignore
+ public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore
+ public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore
+ public get ComponentView() { return this._docViewInternal?._componentView; } // prettier-ignore
+ public get allLinks() { return this._docViewInternal?._allLinks ?? []; } // prettier-ignore
+
+ get LayoutFieldKey() {
+ return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString);
+ }
+
+ @computed get layout_fitWidth() {
+ return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
+ }
+ @computed get anchorViewDoc() {
+ return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined;
}
- public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this._docViewInternal.NativeDimScaling, this._props.PanelWidth(), this._props.PanelHeight());
- public getBounds = () => {
- if (!this._docViewInternal?.ContentDiv || this._props.treeViewDoc || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) {
+ @computed get getBounds() {
+ if (!this._docViewInternal?._contentDiv || this._props.treeViewDoc || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) {
return undefined;
}
if (this._docViewInternal._componentView?.screenBounds?.()) {
return this._docViewInternal._componentView.screenBounds();
}
- const xf = this._docViewInternal._props.ScreenToLocalTransform().scale(this.nativeScaling).inverse();
+ const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
- if (this._docViewInternal._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
- const docuBox = this._docViewInternal.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
+ if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
+ const docuBox = this._docViewInternal._contentDiv.getElementsByClassName('linkAnchorBox-cont');
if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
}
return { left, top, right, bottom };
+ }
+
+ @computed get nativeWidth() {
+ return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
+ }
+ @computed get nativeHeight() {
+ return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth));
+ }
+ @computed public get centeringX() { return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } // prettier-ignore
+ @computed public get centeringY() { return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } // prettier-ignore
+
+ /**
+ * path of DocumentViews hat contains this DocumentView (does not includes this DocumentView thouhg)
+ */
+ public get containerViewPath() { return this._props.containerViewPath; } // prettier-ignore
+ public get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } // prettier-ignore
+ public get CollectionFreeFormDocumentView() { return this._props.CollectionFreeFormDocumentView?.(); } // prettier-ignore
+
+ public clearViewTransition = () => {
+ this._viewTimer && clearTimeout(this._viewTimer);
+ this.layoutDoc._viewTransition = undefined;
};
+ public noOnClick = () => this._docViewInternal?.noOnClick();
+ public makeIntoPortal = () => this._docViewInternal?.makeIntoPortal();
+ public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
+ public setToggleDetail = () => this._docViewInternal?.setToggleDetail();
+ public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
+ public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
+ public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
+ public showContextMenu = (pageX: number, pageY: number) => this._docViewInternal?.onContextMenu(undefined, pageX, pageY);
+
+ public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight());
public iconify(finished?: () => void, animateTime?: number) {
this.ComponentView?.updateIcon?.();
- const animTime = this._docViewInternal?._animateScaleTime;
+ const animTime = this._docViewInternal?.animateScaleTime();
runInAction(() => this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime));
const finalFinished = action(() => {
finished?.();
@@ -1564,13 +1285,58 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
this._props.bringToFront(this.Document);
}
}
- @undoBatch
- setCustomView = (custom: boolean, layout: string): void => {
+
+ public playAnnotation = () => {
+ const self = this;
+ const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
+ const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null);
+ const anno = audioAnnos?.lastElement();
+ if (anno instanceof AudioField) {
+ switch (audioAnnoState) {
+ case 'stopped':
+ this.dataDoc[AudioPlay] = new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
+ });
+ this.dataDoc.audioAnnoState = 'playing';
+ break;
+ case 'playing':
+ this.dataDoc[AudioPlay]?.stop();
+ this.dataDoc.audioAnnoState = 'stopped';
+ break;
+ }
+ }
+ };
+
+ public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
+ this._htmlOverlayText = text;
+ this._htmlOverlayEffect = effect;
+ });
+ public setAnimateScaling = action((scale: number, time?: number) => {
+ if (this._docViewInternal) {
+ this._docViewInternal._animateScalingTo = scale;
+ this._docViewInternal._animateScaleTime = time;
+ }
+ });
+ public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
+ this._animEffectTimer && clearTimeout(this._animEffectTimer);
+ this.Document[Animation] = presEffect;
+ this._animEffectTimer = setTimeout(() => (this.Document[Animation] = undefined), timeInMs);
+ };
+ public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
+ this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans);
+ };
+
+ public setCustomView = undoable((custom: boolean, layout: string): void => {
Doc.setNativeView(this.Document);
custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
- };
+ }, 'set custom view');
- switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+ public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc
setTimeout(
action(() => {
@@ -1585,16 +1351,19 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
this._docViewInternal && (this._docViewInternal._animateScalingTo = 0);
finished?.();
}),
- this._docViewInternal ? Math.max(0, this._docViewInternal.animateScaleTime - 10) : 0
+ Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10)
);
}),
- this._docViewInternal ? Math.max(0, this._docViewInternal?.animateScaleTime - 10) : 0
+ Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10)
);
};
+ /**
+ * @returns a hierarchy path through the nested DocumentViews that display this view. The last element of the path is this view.
+ */
+ public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth);
screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
- docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
isSelected = () => this.IsSelected;
select = (extendSelection: boolean, focusSelection?: boolean) => {
if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this);
@@ -1615,6 +1384,7 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
PanelWidth = () => this.panelWidth;
PanelHeight = () => this.panelHeight;
NativeDimScaling = () => this.nativeScaling;
+ hideLinkCount = () => (this.hideLinkButton ? true : false);
selfView = () => this;
/**
* @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView)
@@ -1629,34 +1399,19 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
.translate(-this.centeringX, -this.centeringY)
.scale(1 / this.nativeScaling);
- componentDidMount() {
- runInAction(() => this.Document[DocViews].add(this));
- this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction);
- !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this);
- }
-
- componentWillUnmount() {
- runInAction(() => this.Document[DocViews].delete(this));
- Object.values(this._disposers).forEach(disposer => disposer?.());
- !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
- }
- // want the htmloverlay to be able to fade in but we also want it to be display 'none' until it is needed.
- // unfortunately, CSS can't transition animate any properties for something that is display 'none'.
- // so we need to first activate the div, then, after a render timeout, start the opacity transition.
- @observable enableHtmlOverlayTransitions: boolean = false;
- @computed get htmlOverlay() {
+ htmlOverlay = () => {
const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect));
return (
<div
className="documentView-htmlOverlay"
ref={r => {
const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
- if (r && val !== this.enableHtmlOverlayTransitions) {
- setTimeout(action(() => (this.enableHtmlOverlayTransitions = val)));
+ if (r && val !== this._enableHtmlOverlayTransitions) {
+ setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
}
}}
style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
- <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this.enableHtmlOverlayTransitions ? 0.9 : 0 }}>
+ <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}>
{DocumentViewInternal.AnimationEffect(
<div className="webBox-textHighlight">
<ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} />
@@ -1667,11 +1422,7 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
</div>
</div>
);
- }
-
- @computed get infoUI() {
- return this.ComponentView?.infoUI?.();
- }
+ };
render() {
TraceMobx();
@@ -1685,14 +1436,14 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
className="contentFittingDocumentView-previewDoc"
ref={this.ContentRef}
style={{
- transition: 'inherit', // this._props.dataTransition,
transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`,
height: this._props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`),
}}>
<DocumentViewInternal
{...this._props}
- docViewPublic={this.selfView}
+ fieldKey={this.LayoutFieldKey}
+ DocumentView={this.selfView}
docViewPath={this.docViewPath}
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
@@ -1706,15 +1457,44 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> {
focus={this._props.focus || emptyFunction}
ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))}
/>
- {this.htmlOverlay}
- {this.infoUI}
+ {this.htmlOverlay()}
+ {this.ComponentView?.infoUI?.()}
</div>
)}
-
- {this.linkCountView}
+ {/* display link count button */}
+ <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} />
</div>
);
}
+
+ public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) {
+ docs.forEach(doc => {
+ doc._viewTransition = `${transProp} ${timeInMs}ms`;
+ dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
+ });
+ timer && clearTimeout(timer);
+ return setTimeout(
+ () =>
+ docs.forEach(doc => {
+ doc._viewTransition = undefined;
+ dataTrans && (doc.dataTransition = 'inherit');
+ afterTrans?.();
+ }),
+ timeInMs + 10
+ );
+ }
+
+ // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
+ public static showBackLinks(linkAnchor: Doc) {
+ const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
+ // prettier-ignore
+ DocServer.GetRefField(docId).then(docx =>
+ LightboxView.Instance.SetLightboxDoc(
+ (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection
+ Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, onViewMounted: ScriptField.MakeScript('updateLinkCollection(this, this.target)') }, docId)
+ )
+ );
+ }
}
ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {