diff options
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 1020 |
1 files changed, 346 insertions, 674 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c1665ea3d..f1f5f7e10 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,11 +1,9 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; -import { Document } from '../../../fields/documentSchemas'; +import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -13,9 +11,8 @@ import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; -import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; -import { MobileInterface } from '../../../mobile/MobileInterface'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnNone, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; +import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; +import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -25,16 +22,14 @@ import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; -import { LinkFollower } from '../../util/LinkFollower'; +import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; 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 { undoBatch, UndoManager } from '../../util/UndoManager'; -import { CollectionView } from '../collections/CollectionView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; @@ -49,11 +44,10 @@ import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { LinkAnchorBox } from './LinkAnchorBox'; -import { RadialMenu } from './RadialMenu'; -import { ScriptingBox } from './ScriptingBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); +import { InkingStroke } from '../InkingStroke'; const { Howl } = require('howler'); interface Window { @@ -75,6 +69,7 @@ export enum OpenWhere { close = 'close', fullScreen = 'fullScreen', toggle = 'toggle', + toggleRight = 'toggle:right', replace = 'replace', replaceRight = 'replace:right', replaceLeft = 'replace:left', @@ -88,11 +83,12 @@ export enum OpenWhereMod { right = 'right', top = 'top', bottom = 'bottom', + rightKeyValue = 'rightKeyValue', } export interface DocFocusOptions { willPan?: boolean; // determines whether to pan to target document - willZoomCentered?: boolean; // determines whether to zoom in on 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 @@ -102,7 +98,7 @@ export interface DocFocusOptions { effect?: Doc; // animation effect for focus noSelect?: boolean; // whether target should be selected after focusing playAudio?: boolean; // whether to play audio annotation on focus - openLocation?: string; // where to open a missing document + 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 @@ -113,14 +109,17 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p export interface DocComponentView { 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 }) => void; getView?: (doc: Doc) => 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 reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views + select?: (ctrlKey: boolean, shiftKey: boolean) => void; menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. 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; @@ -130,6 +129,8 @@ export interface DocComponentView { setFocus?: () => void; // sets input focus to the componentView componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; + fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) + overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document fieldKey?: string; annotationKey?: string; getTitle?: () => string; @@ -148,11 +149,8 @@ export interface DocumentViewSharedProps { DataDoc?: Doc; contentBounds?: () => undefined | { x: number; y: number; r: number; b: number }; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document - ContainingCollectionView: Opt<CollectionView>; - ContainingCollectionDoc: Opt<Doc>; suppressSetHeight?: boolean; thumbShown?: () => boolean; - isHovering?: () => boolean; setContentView?: (view: DocComponentView) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; PanelWidth: () => number; @@ -173,13 +171,14 @@ export interface DocumentViewSharedProps { rootSelected: (outsideReaction?: boolean) => 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[]) => boolean; - removeDocument?: (doc: Doc | Doc[]) => boolean; - moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; + 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; canEmbedOnDrag?: boolean; + treeViewDoc?: Doc; xPadding?: number; yPadding?: number; dropAction?: dropActionType; @@ -189,6 +188,10 @@ export interface DocumentViewSharedProps { ignoreAutoHeight?: boolean; forceAutoHeight?: boolean; disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. + onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + enableDragWhenActive?: boolean; + waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; + defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document createNewFilterDoc?: () => void; @@ -206,14 +209,13 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideDocumentButtonBar?: boolean; hideOpenButton?: boolean; hideDeleteButton?: boolean; - treeViewDoc?: Doc; + hideLinkAnchors?: boolean; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents radialMenu?: String[]; LayoutTemplateString?: string; dontCenter?: 'x' | 'y' | 'xy'; - dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling NativeWidth?: () => number; NativeHeight?: () => number; NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps @@ -232,8 +234,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { NativeWidth: () => number; NativeHeight: () => number; isSelected: (outsideReaction?: boolean) => boolean; - isHovering: () => boolean; - select: (ctrlPressed: boolean) => void; + select: (ctrlPressed: boolean, shiftPress?: boolean) => void; DocumentView: () => DocumentView; viewPath: () => DocumentView[]; } @@ -241,19 +242,17 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() { public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. - private _cursorTimer: NodeJS.Timeout | undefined; - private _longPress = false; private _disposers: { [name: string]: IReactionDisposer } = {}; + private _doubleClickTimeout: NodeJS.Timeout | undefined; + private _singleClickFunc: undefined | (() => any); + private _longPressSelector: NodeJS.Timeout | undefined; private _downX: number = 0; private _downY: number = 0; private _downTime: number = 0; - private _firstX: number = -1; - private _firstY: number = -1; private _lastTap: number = 0; private _doubleTap = false; private _mainCont = React.createRef<HTMLDivElement>(); private _titleRef = React.createRef<EditableView>(); - private _timeout: NodeJS.Timeout | undefined; private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @@ -261,14 +260,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; - @observable _pendingDoubleClick = false; - @observable _cursorPress = false; - private get topMost() { - return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; - } public get animateScaleTime() { - return this._animateScaleTime ?? 300; + return this._animateScaleTime ?? 100; } public get displayName() { return 'DocumentView(' + this.props.Document.title + ')'; @@ -288,9 +282,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get thumb() { return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); } - @computed get hidden() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); - } @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); } @@ -303,8 +294,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : '')); } - @computed get backgroundColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); + @computed get backgroundBoxColor() { + return this.thumbShown() ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box'); } @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); @@ -312,6 +303,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } + @computed get showCaption() { + return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0; + } @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } @@ -327,6 +321,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get nativeHeight() { return this.props.NativeHeight(); } + @computed get disableClickScriptFunc() { + const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; + // prettier-ignore + return ( + DocumentView.LongPress || + onScriptDisable === 'always' || + (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) || + this._componentView?.isAnyChildContentActive?.() + ); + } @computed get onClickHandler() { return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @@ -346,7 +350,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps componentDidMount() { this.setupHandlers(); } - //componentDidUpdate() { this.setupHandlers(); } + setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { @@ -364,177 +368,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Object.values(this._disposers).forEach(disposer => disposer?.()); } - handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { - this.removeMoveListeners(); - this.removeEndListeners(); - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); - if (RadialMenu.Instance._display === false) { - this.addHoldMoveListeners(); - this.addHoldEndListeners(); - this.onRadialMenu(e, me); - const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - this._firstX = pt.pageX; - this._firstY = pt.pageY; - } - }; - - handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { - const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - - if (this._firstX === -1 || this._firstY === -1) { - return; - } - if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { - this.handle1PointerHoldEnd(e, me); - } - }; - - handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { - this.removeHoldMoveListeners(); - this.removeHoldEndListeners(); - RadialMenu.Instance.closeMenu(); - this._firstX = -1; - this._firstY = -1; - SelectionManager.DeselectAll(); - me.touchEvent.stopPropagation(); - me.touchEvent.preventDefault(); - e.stopPropagation(); - if (RadialMenu.Instance.used) { - this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY); - } - }; - - handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { - if (!this.props.isSelected()) { - e.stopPropagation(); - e.preventDefault(); - - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - } - }; - - handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { - SelectionManager.DeselectAll(); - if (this.Document.onPointerDown) return; - const touch = me.touchEvent.changedTouches.item(0); - if (touch) { - this._downX = touch.clientX; - this._downY = touch.clientY; - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - e.stopPropagation(); - } - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - e.stopPropagation(); - } - }; - - handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { - if (e.cancelBubble && this.props.isDocumentActive?.()) { - this.removeMoveListeners(); - } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - const touch = me.touchEvent.changedTouches.item(0); - if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { - if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { - this.cleanUpInteractions(); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined); - } - } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - } - }; - - @action - handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { - const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); - const pt1 = myTouches[0]; - const pt2 = myTouches[1]; - const oldPoint1 = this.prevPoints.get(pt1.identifier); - const oldPoint2 = this.prevPoints.get(pt2.identifier); - const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); - if (pinching !== 0 && oldPoint1 && oldPoint2) { - const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX); - const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY); - const dX = -1 * Math.sign(dW); - const dY = -1 * Math.sign(dH); - - if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { - const doc = Document(this.props.Document); - const layoutDoc = Document(Doc.Layout(this.props.Document)); - let nwidth = Doc.NativeWidth(layoutDoc); - let nheight = Doc.NativeHeight(layoutDoc); - const width = layoutDoc._width || 0; - const height = layoutDoc._height || (nheight / nwidth) * width; - const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling; - const actualdW = Math.max(width + dW * scale, 20); - const actualdH = Math.max(height + dH * scale, 20); - doc.x = (doc.x || 0) + dX * (actualdW - width); - doc.y = (doc.y || 0) + dY * (actualdH - height); - const fixedAspect = e.ctrlKey || (nwidth && nheight); - if (fixedAspect && (!nwidth || !nheight)) { - Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0)); - Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0)); - } - if (nwidth > 0 && nheight > 0) { - if (Math.abs(dW) > Math.abs(dH)) { - if (!fixedAspect) { - Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc)); - } - layoutDoc._width = actualdW; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width; - else layoutDoc._height = actualdH; - } else { - if (!fixedAspect) { - Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc)); - } - layoutDoc._height = actualdH; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height; - else layoutDoc._width = actualdW; - } - } else { - dW && (layoutDoc._width = actualdW); - dH && (layoutDoc._height = actualdH); - dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); - } - } - e.stopPropagation(); - e.preventDefault(); - } - }; - - @action - onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { - const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); - - // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 }); - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && - RadialMenu.Instance.addItem({ - description: 'Delete', - event: () => { - this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); - }, - icon: 'external-link-square-alt', - selected: -1, - }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 }); - RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 }); - RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 }); - - SelectionManager.DeselectAll(); - }; - startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { if (this._mainCont.current) { - const dragData = new DragManager.DocumentDragData([this.props.Document]); + const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current); + const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()]; + const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc)); const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0); dragData.offset = this.props .ScreenToLocalTransform() @@ -547,8 +385,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.canEmbed = this.props.canEmbedOnDrag; const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () => - setTimeout(action(() => ffview && (ffview.ChildDrag = undefined))) + DragManager.StartDocumentDrag( + selected.map(dv => dv.docView!._mainCont.current!), + dragData, + x, + y, + { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, + () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined))) ); // this needs to happen after the drop event is processed. ffview?.setupDragLines(false); } @@ -557,7 +400,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => { const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document - (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties applie to this document + (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document ? true : false; return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; @@ -569,94 +412,86 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name); (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); - if (this._doubleTap && (![DocumentType.FONTICON, DocumentType.PRES].includes(this.props.Document.type as any) || this.onDoubleClickHandler)) { - // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click - if (this._timeout) { - clearTimeout(this._timeout); - this._pendingDoubleClick = false; - this._timeout = undefined; - } - if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { - // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; - const func = () => - this.onDoubleClickHandler.script.run( - { - this: this.layoutDoc, - self: this.rootDoc, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, - clientY, - altKey, - shiftKey, - ctrlKey, - value: undefined, - }, - console.log - ); + if (this._doubleTap) { + const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; + if (this.onDoubleClickHandler?.script) { + const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables + // prettier-ignore + const func = () => this.onDoubleClickHandler.script.run( { + this: this.layoutDoc, + self: this.rootDoc, + scriptContext: this.props.scriptContext, + documentView: this.props.DocumentView(), + clientX, clientY, altKey, shiftKey, ctrlKey, + value: undefined, + }, console.log ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); - } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) { - //UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap'); + } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) { UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); + } else { + this._singleClickFunc?.(); } - } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) { - // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey, altKey } = e; - const func = () => - this.onClickHandler.script.run( - { - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, - clientY, - shiftKey, - altKey, - }, - console.log - ).result?.select === true - ? this.props.select(false) - : ''; - const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); - if (this.onDoubleClickHandler && !this.props.Document.allowClickBeforeDoubleClick) { - runInAction(() => (this._pendingDoubleClick = true)); - this._timeout = setTimeout(() => { - this._timeout = undefined; - clickFunc(); - }, 150); - } else clickFunc(); - } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { - SelectionManager.DeselectAll(); - this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, e.altKey); + this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); + this._doubleClickTimeout = undefined; + this._singleClickFunc = undefined; } else { - if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { + let clickFunc: undefined | (() => any); + if (!this.disableClickScriptFunc && this.onClickHandler?.script) { + const { clientX, clientY, shiftKey, altKey, metaKey } = e; + const func = () => { + // replace default add doc func with this view's add doc func. + // to allow override behaviors for how to display links to undisplayed documents. + // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place + // instead of in the global lightbox + const oldFunc = DocumentViewInternal.addDocTabFunc; + DocumentViewInternal.addDocTabFunc = this.props.addDocTab; + const res = + this.onClickHandler?.script.run( + { + this: this.layoutDoc, + self: this.rootDoc, + _readOnly_: false, + scriptContext: this.props.scriptContext, + documentView: this.props.DocumentView(), + clientX, + clientY, + shiftKey, + altKey, + metaKey, + }, + console.log + ).result?.select === true + ? this.props.select(false) + : ''; + DocumentViewInternal.addDocTabFunc = oldFunc; + }; + clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); + } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part - stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template + if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { + stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template + } + preventDefault = false; + } + + this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); + const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; + if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { + this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); + this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); } else { - runInAction(() => (this._pendingDoubleClick = true)); - this._timeout = setTimeout( - action(() => { - this._pendingDoubleClick = false; - this._timeout = undefined; - }), - 350 - ); - this.props.select(e.ctrlKey || e.shiftKey); + this._singleClickFunc(); + this._singleClickFunc = undefined; } - preventDefault = false; } stopPropagate && e.stopPropagation(); preventDefault && e.preventDefault(); @@ -665,70 +500,44 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerDown = (e: React.PointerEvent): void => { - if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView(); - if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return; - // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document) - if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) { - if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - e.stopPropagation(); - if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it - // TODO: check here for panning/inking - } - return; - } - this._cursorTimer = setTimeout( - action(() => { - this._cursorPress = true; - this.props.select(false); - }), - 1000 // long press required duration - ); + this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000); + if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); + this._downX = e.clientX; this._downY = e.clientY; this._downTime = Date.now(); if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { - // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking + // click events stop here if the document is active and no modes are overriding it + // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( - (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && + // prettier-ignore + this.props.isDocumentActive?.() && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && - !e.ctrlKey && - (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && + e.button === 0 && + this.pointerEvents !== 'none' && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); + + // listen to move events if document content isn't active or document is draggable + if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) { + document.addEventListener('pointermove', this.onPointerMove); + } } - if (this.props.isDocumentActive?.()) { - document.removeEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointermove', this.onPointerMove); - } - document.removeEventListener('pointerup', this.onPointerUp); document.addEventListener('pointerup', this.onPointerUp); - } else { - this._cursorTimer && clearTimeout(this._cursorTimer); - this._cursorPress = false; } }; @action onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) return; - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; - - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); - this._cursorTimer && clearTimeout(this._cursorTimer); - this._cursorPress = false; - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); - } - } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); + if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; + + if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { + this.cleanupPointerEvents(); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); } }; @@ -741,65 +550,38 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); - this._longPress = this._cursorPress; - this._cursorTimer && clearTimeout(this._cursorTimer); - this._cursorPress = false; + this._longPressSelector && clearTimeout(this._longPressSelector); - const now = Date.now(); - if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + if (this.onPointerUpHandler?.script) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); - } else if (now - this._downTime < 300) { - this._doubleTap = now - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2; - // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them - if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected + } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME; + if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } + if (DocumentView.LongPress) e.preventDefault(); }; @undoBatch @action toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => { - this.Document.ignoreClick = false; - if (setTargetToggle) { - this.Document.followLinkToggle = !this.Document.followLinkToggle; - this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton; - } else { - this.Document._isLinkButton = !this.Document._isLinkButton; - } - if (this.Document._isLinkButton && !this.onClickHandler) { - zoom !== undefined && (this.Document.followLinkZoom = zoom); - } else if (this.Document._isLinkButton && this.onClickHandler) { - this.Document._isLinkButton = false; - this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; - } - }; - @undoBatch - @action - toggleTargetOnClick = (): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = true; - this.Document.followLinkToggle = true; - }; - @undoBatch - @action - followLinkOnClick = (location: Opt<string>, zoom: boolean): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = true; - this.Document.followLinkToggle = false; - this.Document.followLinkZoom = zoom; - this.Document.followLinkLocation = location; + const hadOnClick = this.rootDoc.onClick; + this.noOnClick(); + this.Document.onClick = hadOnClick ? undefined : FollowLinkScript(); + this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never'; }; @undoBatch @action - selectOnClick = (): void => { + followLinkOnClick = (): void => { this.Document.ignoreClick = false; - this.Document._isLinkButton = false; + this.Document.onClick = FollowLinkScript(); this.Document.followLinkToggle = false; - this.Document.onClick = this.layoutDoc.onClick = undefined; + this.Document.followLinkZoom = false; + this.Document.followLinkLocation = undefined; }; @undoBatch noOnClick = (): void => { this.Document.ignoreClick = false; - this.Document._isLinkButton = false; + this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined; }; @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); @@ -813,22 +595,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action - drop = async (e: Event, de: DragManager.DropEvent) => { + drop = (e: Event, de: DragManager.DropEvent) => { if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; if (this.props.Document === Doc.ActiveDashboard) { alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); return; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; - if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); - if (linkdrag?.linkSourceDoc) { - e.stopPropagation(); - if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { - de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); - } - if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { - const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document; - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); + if (linkdrag) { + linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); + if (linkdrag.linkSourceDoc) { + e.stopPropagation(); + if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { + de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); + } + if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { + const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; + de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); + } } } }; @@ -836,17 +620,34 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action makeIntoPortal = () => { - const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document); + const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( - { doc: this.props.Document }, - { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) }, - 'portal to:portal from' + this.props.Document, + Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }), + { linkRelationship: 'portal to:portal from' } ); } this.Document.followLinkLocation = OpenWhere.lightbox; - this.Document.followLinkZoom = true; - this.Document._isLinkButton = true; + this.Document.onClick = FollowLinkScript(); + }; + + importDocument = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.zip'; + input.onchange = _e => { + if (input.files) { + const batch = UndoManager.StartBatch('importing'); + Doc.importDocument(input.files[0]).then(doc => { + if (doc instanceof Doc) { + this.props.addDocTab(doc, OpenWhere.addRight); + batch.end(); + } + }); + } + }; + input.click(); }; @action @@ -900,27 +701,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const appearance = cm.findByDescription('UI Controls...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); - !Doc.noviceMode && - appearanceItems.push({ - description: 'Add a Field', - event: () => { - const alias = Doc.MakeAlias(this.rootDoc); - alias.layout = FormattedTextBox.LayoutString('newfield'); - alias.title = 'newfield'; - alias._height = 35; - alias._width = 100; - alias.syncLayoutFieldWithTitle = true; - alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); - alias.y = NumCast(this.rootDoc.y); - this.props.addDocument?.(alias); - }, - icon: 'eye', - }); - LinkManager.Links(this.Document).length && - appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' }); - !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); + !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; @@ -934,33 +717,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))), icon: 'hand-point-up', }); - !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' }); + !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' }); } - !Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); + onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); - this.props.CollectionFreeFormDocumentView && - onClicks.push({ - description: (this.Document.followLinkZoom ? "Don't" : '') + ' zoom following link', - event: () => (this.Document.followLinkZoom = !this.Document.followLinkZoom), - icon: this.Document.ignoreClick ? 'unlock' : 'lock', - }); if (!this.Document.annotationOn) { const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); - onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(false, true), icon: 'map-pin' }); + onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { - onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' }); - onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' }); - onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' }); + onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' }); + onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); } } @@ -986,20 +760,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); } - moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); } - if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) { + !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); + } + const constantItems: ContextMenuProps[] = []; + constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' }); + if (!Doc.IsSystem(this.rootDoc)) { + constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); + constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() }); + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); + if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) - moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); + constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } - !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); } + cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' }); + const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); @@ -1038,43 +817,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } // Add link to help documentation if (documentationDescription && documentationLink) { - console.log('add documentation item'); helpItems.push({ description: documentationDescription, - event: () => { - window.open(documentationLink, '_blank'); - }, + event: () => window.open(documentationLink, '_blank'), icon: 'book', }); } - cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); + if (!help) cm.addItem({ description: 'Help...', noexpand: !Doc.noviceMode, subitems: helpItems, icon: 'question' }); + else cm.moveAfter(help); - if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event + e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); }; - collectionFilters = () => StrListCast(this.props.Document._docFilters); - collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); - @computed get showFilterIcon() { - return this.collectionFilters().length || this.collectionRangeDocFilters().length - ? 'hasFilter' - : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length - ? 'inheritsFilter' - : undefined; - } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); - onClickFunc = () => this.onClickHandler; + onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - isContentActive = (outsideReaction?: boolean) => { + isContentActive = (outsideReaction?: boolean): boolean | undefined => { return this.props.isContentActive() === false ? false : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || - this.props.Document.forceActive || + this.rootDoc.forceActive || this.props.isSelected(outsideReaction) || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() @@ -1088,7 +856,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc)); - return !this.props.isSelected() && + return !this.props.LayoutTemplateString && + !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && @@ -1097,36 +866,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ? true : false; }; - get audioAnnoState() { - return this.dataDoc.audioAnnoState ?? 'stopped'; - } - @computed get audioAnnoView() { - const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length; - const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); - const audioIconColors = new Map<string, string>([ - ['recording', 'red'], - ['playing', 'green'], - ['stopped', audioAnnosCount ? 'blue' : 'gray'], - ]); - return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : ( - <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> - <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> - <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" /> - </div> - </Tooltip> - ); - } - pointerEventsFunc = () => this.pointerEvents; + docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)]; + contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents); @computed get contents() { TraceMobx(); + const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; return ( <div className="documentView-contentsView" style={{ - // prettier-ignore - pointerEvents: - (this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents) ?? - (this.rootDoc.layoutKey === 'layout_icon' ? 'none' : 'all'), + pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all', height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? null : ( @@ -1137,21 +886,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps height={this.props.PanelHeight()} onError={(e: any) => { setTimeout(action(() => (this._retryThumb = 0))); - setTimeout( - action(() => (this._retryThumb = 1)), - 150 - ); + // prettier-ignore + setTimeout(action(() => (this._retryThumb = 1)), 150 ); }} /> )} <DocumentContentsView key={1} {...this.props} - pointerEvents={this.opacity === 0 ? returnNone : this.pointerEventsFunc} + pointerEvents={this.contentPointerEvents} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} - isHovering={this.props.isHovering} setContentView={this.setContentView} + docFilters={this.docFilters} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} @@ -1164,19 +911,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} - {this.audioAnnoView} </div> ); } - get indicatorIcon() { - if (this.props.Document['acl-Public'] !== SharingPermissions.None) return 'globe-americas'; - else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return 'users'; - else return 'user'; - } - - @undoBatch - hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true); anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { @@ -1184,7 +922,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps switch (property.split(':')[0]) { case StyleProp.ShowTitle: return ''; case StyleProp.PointerEvents: return 'none'; - case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox case StyleProp.Highlighting: return undefined; } return this.props.styleProvider?.(doc, props, property); @@ -1209,26 +946,28 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } + hideLink = computedFn((link: Doc) => () => (link.linkDisplay = false)); @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); - if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; - if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null; + if (this.props.hideLinkAnchors || this.layoutDoc.hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null; const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); - return filtered.map((link, i) => ( + return filtered.map(link => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView {...this.props} isContentActive={returnFalse} Document={link} + docViewPath={this.props.viewPath} PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} showTitle={returnEmptyString} hideCaptions={true} + hideLinkAnchors={true} fitWidth={returnTrue} + removeDocument={this.hideLink(link)} styleProvider={this.anchorStyleProvider} - removeDocument={this.hideLinkAnchor} LayoutTemplate={undefined} LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} /> @@ -1236,26 +975,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps )); } - @action - playAnnotation = () => { - const self = this; - const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); - const anno = audioAnnos?.lastElement(); - if (anno instanceof AudioField && this.audioAnnoState === 'stopped') { - new Howl({ - src: [anno.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => (self.dataDoc.audioAnnoState = 'stopped')); - }, - }); - this.dataDoc.audioAnnoState = 'playing'; - } - }; - static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { let gumStream: any; let recorder: any; @@ -1303,34 +1022,50 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 && audioAnnoState === 'stopped') { + 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'; + } + }; - captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); + captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); @computed get innards() { TraceMobx(); const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; const showTitle = this.ShowTitle?.split(':')[0]; const showTitleHover = this.ShowTitle?.includes(':hover'); - const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; - const captionView = !showCaption ? null : ( + const captionView = !this.showCaption ? null : ( <div className="documentView-captionWrapper" style={{ - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, minWidth: 50 * ffscale(), maxHeight: `max(100%, ${20 * ffscale()}px)`, }}> <FormattedTextBox - {...OmitKeys(this.props, ['children']).omit} + {...this.props} yPadding={10} xPadding={10} - fieldKey={showCaption} + fieldKey={this.showCaption} fontSize={12 * Math.max(1, (2 * ffscale()) / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} noSidebar={true} dontScale={true} + renderDepth={this.props.renderDepth} isContentActive={this.isContentActive} - onClick={this.onClickFunc} /> </div> ); @@ -1349,7 +1084,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button color: lightOrDark(background), background, - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> <EditableView ref={this._titleRef} @@ -1382,7 +1117,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps /> </div> ); - return this.props.hideTitle || (!showTitle && !showCaption) ? ( + return this.props.hideTitle || (!showTitle && !this.showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> @@ -1394,34 +1129,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); } - @observable _: string = ''; + renderDoc = (style: object) => { TraceMobx(); - const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); - const isButton = this.props.Document.type === DocumentType.FONTICON; - if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null; - return ( - this.docContents ?? ( - <div - className={`documentView-node${this.topMost ? '-topmost' : ''}`} - id={this.props.Document[Id]} - style={{ - ...style, - background: isButton || thumb ? undefined : this.backgroundColor, - opacity: this.opacity, - cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', - color: StrCast(this.layoutDoc.color, 'inherit'), - fontFamily: StrCast(this.Document._fontFamily, 'inherit'), - fontSize: Cast(this.Document._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'}`, - }}> - {this.innards} - {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null} - {this.widgetDecorations ?? null} - </div> - ) - ); + return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate + ? null + : this.docContents ?? ( + <div + className="documentView-node" + id={this.Document[Id]} + style={{ + ...style, + background: this.backgroundBoxColor, + opacity: this.opacity, + cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', + color: StrCast(this.layoutDoc.color, 'inherit'), + fontFamily: StrCast(this.Document._fontFamily, 'inherit'), + fontSize: Cast(this.Document._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'}`, + }}> + {this.innards} + {this.widgetDecorations ?? null} + </div> + ); }; /** @@ -1456,7 +1187,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps render() { TraceMobx(); const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); - const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; + const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath); const boxShadow = this.props.treeViewDoc || !highlighting ? this.boxShadow @@ -1468,10 +1199,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, boxShadow, - clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined, + clipPath: borderPath?.clipPath, }); - const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc); return ( <div className={`${DocumentView.ROOT_DIV} docView-hack`} @@ -1483,34 +1213,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)} style={{ - display: this.hidden ? 'inline' : undefined, borderRadius: this.borderRounding, pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, }}> - {!borderPath.path ? ( - animRenderDoc - ) : ( - <> - {animRenderDoc} - <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> - <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> - <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> - </svg> - </div> - </> - )} - {this.showFilterIcon ? ( - <FontAwesomeIcon - icon={'filter'} - size="lg" - style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: this.showFilterIcon === 'hasFilter' ? '#18c718bd' : 'orange', zIndex: 1 }} - onPointerDown={action(e => { - this.props.select(false); - SettingsManager.propertiesWidth = 250; - e.stopPropagation(); - })} - /> - ) : null} + <> + {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)} + {borderPath?.jsx} + </> </div> ); } @@ -1519,6 +1228,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observer export class DocumentView extends React.Component<DocumentViewProps> { public static ROOT_DIV = 'documentView-effectsWrapper'; + @observable public static LongPress = false; + @observable public docView: DocumentViewInternal | undefined | null; + @observable public textHtmlOverlay: Opt<string>; + @observable private _isHovering = false; + + public htmlOverlayEffect = ''; public get displayName() { return 'DocumentView(' + this.props.Document?.title + ')'; } // this makes mobx trace() statements more descriptive @@ -1530,6 +1245,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { this.ViewTimer && clearTimeout(this.ViewTimer); this.rootDoc._viewTransition = undefined; }; + public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + + public showContextMenu = (pageX: number, pageY: number) => this.docView?.onContextMenu(undefined, pageX, pageY); + public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer); this.rootDoc[AnimationSym] = presEffect; @@ -1561,34 +1280,18 @@ export class DocumentView extends React.Component<DocumentViewProps> { ); } + // shows a stacking view collection (by default, but the user can change) of all documents linked to the source public static showBackLinks(linkSource: Doc) { const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; - DocServer.GetRefField(docId).then(docx => { - const rootAlias = () => { - const rootAlias = Doc.MakeAlias(linkSource); - rootAlias.x = rootAlias.y = 0; - return rootAlias; - }; - const linkCollection = - (docx instanceof Doc && docx) || - Docs.Create.StackingDocument( - [ - /*rootAlias()*/ - ], - { title: linkSource.title + '-pivot', _width: 500, _height: 500 }, - docId - ); - linkCollection.linkSource = linkSource; - if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)'); - LightboxView.SetLightboxDoc(linkCollection); - }); + // prettier-ignore + DocServer.GetRefField(docId).then(docx => docx instanceof Doc && + LightboxView.SetLightboxDoc( + docx || // reuse existing pivot view of documents, or else create a new collection + Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId) + ) + ); } - @observable public docView: DocumentViewInternal | undefined | null; - - showContextMenu(pageX: number, pageY: number) { - return this.docView?.onContextMenu(undefined, pageX, pageY); - } get Document() { return this.props.Document; } @@ -1596,13 +1299,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.renderDepth === 0; } get rootDoc() { - return this.docView?.rootDoc || this.Document; + return this.docView?.rootDoc ?? this.Document; } get dataDoc() { - return this.docView?.dataDoc || this.Document; - } - get finalLayoutKey() { - return this.docView?.finalLayoutKey || 'layout'; + return this.docView?.dataDoc ?? this.Document; } get ContentDiv() { return this.docView?.ContentDiv; @@ -1616,20 +1316,19 @@ export class DocumentView extends React.Component<DocumentViewProps> { get LayoutFieldKey() { return this.docView?.LayoutFieldKey || 'layout'; } - get fitWidth() { - return this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth; + @computed get fitWidth() { + return this.docView?._componentView?.fitWidth?.() ?? this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth; + } + @computed get anchorViewDoc() { + return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined; } - @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : '')); } - linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale; - @computed get linkCountView() { const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; - return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />; + return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; } - @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @@ -1643,7 +1342,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); } @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1659,12 +1358,11 @@ export class DocumentView extends React.Component<DocumentViewProps> { } return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } - @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } @computed get panelHeight() { - if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) { + if (this.effectiveNativeHeight && (!this.fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) { return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); } return this.props.PanelHeight(); @@ -1684,12 +1382,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; } - toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); - getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); + public getBounds = () => { + if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } - const xf = this.docView?.props + const xf = this.docView.props .ScreenToLocalTransform() .scale(this.trueNativeWidth() ? this.nativeScaling : 1) .inverse(); @@ -1726,7 +1424,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { Doc.setNativeView(this.props.Document); custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); }; - switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + @action + switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc setTimeout( action(() => { @@ -1746,17 +1445,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { }), this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0 ); - }); - - startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + }; - @observable textHtmlOverlay: Opt<string>; - @computed get anchorViewDoc() { - return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined; - } + scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); - select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); + select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection); NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; PanelWidth = () => this.panelWidth; @@ -1770,11 +1464,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { .translate(-this.centeringX, -this.centeringY) .scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1); componentDidMount() { - this._disposers.reactionScript = reaction( - () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, + this._disposers.updateContentsScript = reaction( + () => ScriptCast(this.rootDoc.updateContentsScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, output => output ); this._disposers.height = reaction( + // increase max auto height if document has been resized to be greater than current max () => NumCast(this.layoutDoc._height), action(height => { const docMax = NumCast(this.layoutDoc.docMaxAutoHeight); @@ -1787,24 +1482,20 @@ export class DocumentView extends React.Component<DocumentViewProps> { Object.values(this._disposers).forEach(disposer => disposer?.()); !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } - _hoverTimeout: any = undefined; - isHovering = () => this._isHovering; - @observable _isHovering = false; - - htmlOverlayEffect = ''; @computed get htmlOverlay() { - const effectProps = { - delay: 0, - duration: 500, - }; - const highlight = ( - <div className="webBox-textHighlight"> - <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} /> - </div> - ); return !this.textHtmlOverlay ? null : ( <div className="documentView-htmlOverlay"> - <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div> + <div className="documentView-htmlOverlayInner"> + <Fade delay={0} duration={500}> + {DocumentViewInternal.AnimationEffect( + <div className="webBox-textHighlight"> + <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} /> + </div>, + { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, + this.rootDoc + )}{' '} + </Fade> + </div> </div> ); } @@ -1813,34 +1504,20 @@ export class DocumentView extends React.Component<DocumentViewProps> { TraceMobx(); const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; - const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; return ( - <div - className="contentFittingDocumentView" - onPointerEnter={action(() => { - clearTimeout(this._hoverTimeout); - this._isHovering = true; - })} - onPointerLeave={action(() => { - clearTimeout(this._hoverTimeout); - this._hoverTimeout = setTimeout( - action(() => (this._isHovering = false)), - 500 - ); - })}> + <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> {!this.props.Document || !this.props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ transition: this.props.dataTransition, - transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, - height: - isButton || this.props.forceAutoHeight - ? undefined - : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, + width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, + height: this.props.forceAutoHeight + ? undefined + : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} @@ -1853,7 +1530,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeDimScaling={this.NativeDimScaling} isSelected={this.isSelected} select={this.select} - isHovering={this.isHovering} ScreenToLocalTransform={this.screenToLocalTransform} focus={this.props.focus || emptyFunction} ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} @@ -1868,10 +1544,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } -export function deiconifyViewFunc(documentView: DocumentView) { - documentView.iconify(); - //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc); -} ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { documentView.iconify(); documentView.select(false); |