diff options
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 430 |
1 files changed, 298 insertions, 132 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5b26469ed..1df46488b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,9 @@ 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 { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { ObserverJsxParser } from './DocumentContentsView'; +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 { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -11,7 +13,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +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'; @@ -51,9 +53,9 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkDocPreview } from './LinkDocPreview'; import { RadialMenu } from './RadialMenu'; import { ScriptingBox } from './ScriptingBox'; -import { PresBox } from './trails/PresBox'; +import { PresEffect, PresEffectDirection } from './trails'; +import { PinProps } from './trails/PresBox'; import React = require('react'); -import { CollectionTreeView } from '../collections/CollectionTreeView'; const { Howl } = require('howler'); interface Window { @@ -70,15 +72,49 @@ export enum ViewAdjustment { doNothing = 0, } +export enum OpenWhere { + inPlace = 'inPlace', + lightbox = 'lightbox', + add = 'add', + addLeft = 'add:left', + addRight = 'add:right', + addBottom = 'add:bottom', + dashboard = 'dashboard', + close = 'close', + fullScreen = 'fullScreen', + toggle = 'toggle', + replace = 'replace', + replaceRight = 'replace:right', + replaceLeft = 'replace:left', + inParent = 'inParent', + inParentFromScreen = 'inParentFromScreen', +} +export enum OpenWhereMod { + none = '', + left = 'left', + right = 'right', + top = 'top', + bottom = 'bottom', +} + export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document) export interface DocFocusOptions { originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab - willZoom?: boolean; // determines whether to zoom in on target document - scale?: number; // percent of containing frame to zoom into document + willPan?: boolean; // determines whether to pan to target document + willPanZoom?: boolean; // determines whether to zoom in on target document + zoomScale?: number; // percent of containing frame to zoom into document + zoomTime?: number; afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document 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) + effect?: Doc; // animation effect for focus + noSelect?: boolean; // whether target should be selected after focusing + playAudio?: boolean; // whether to play audio annotation on focus + zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections + toggleTarget?: boolean; // whether to toggle target on and off + originatingDoc?: Doc; // document that triggered the focus + easeFunc?: 'linear' | 'ease'; // transition method for scrolling } export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>; export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void; @@ -86,7 +122,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p export interface DocComponentView { updateIcon?: () => void; // updates the icon representation of the document getAnchor?: () => 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) - scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus + scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document 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. @@ -99,6 +135,7 @@ export interface DocComponentView { Pause?: () => void; setFocus?: () => void; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; + incrementalRendering?: () => void; fieldKey?: string; annotationKey?: string; getTitle?: () => string; @@ -140,14 +177,15 @@ export interface DocumentViewSharedProps { showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: string) => boolean; + 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; - pinToPres: (document: Doc) => void; + pinToPres: (document: Doc, pinProps: PinProps) => void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; + canEmbedOnDrag?: boolean; xPadding?: number; yPadding?: number; dropAction?: dropActionType; @@ -167,7 +205,8 @@ export interface DocumentViewSharedProps { // these props are specific to DocuentViews export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView - hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected + 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 hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDocumentButtonBar?: boolean; @@ -199,6 +238,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { NativeWidth: () => number; NativeHeight: () => number; isSelected: (outsideReaction?: boolean) => boolean; + isHovering: () => boolean; select: (ctrlPressed: boolean) => void; DocumentView: () => DocumentView; viewPath: () => DocumentView[]; @@ -207,9 +247,8 @@ 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. - _animateScaleTime = 300; // milliseconds; - @observable _animateScalingTo = 0; - @observable _pendingDoubleClick = false; + private _cursorTimer: NodeJS.Timeout | undefined; + private _longPress = false; private _disposers: { [name: string]: IReactionDisposer } = {}; private _downX: number = 0; private _downY: number = 0; @@ -223,11 +262,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + @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; + } public get displayName() { return 'DocumentView(' + this.props.Document.title + ')'; } // this makes mobx trace() statements more descriptive @@ -258,9 +305,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } - @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ':selected' : '')); - } @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : '')); } @@ -475,7 +519,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 }), "add:right"), icon: "map-pin", selected: -1 }); + // 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({ @@ -486,8 +530,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps icon: 'external-link-square-alt', selected: -1, }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), 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 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(); @@ -507,6 +551,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; + dragData.canEmbed = this.props.canEmbedOnDrag; //dragData.dimSource : // dragEffects field, set dim // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping @@ -527,7 +572,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps e.preventDefault(); if (e.key === '†' || e.key === 't') { if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; - if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); + if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true)); else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... this._titleRef.current?.setIsFocused(false); @@ -542,16 +587,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // copying over VIEW fields immediately allows the view type to switch to create the right _componentView Array.from(Object.keys(Doc.GetProto(anchor))) .filter(key => key.startsWith(ViewSpecPrefix)) - .forEach(spec => { - this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]); - }); - // after a timeout, the right _componentView should have been created, so call it to update its view spec values + .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]))); + // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false }); const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing; + const startTime = Date.now(); this.props.focus(options?.docTransform ? anchor : this.rootDoc, { ...options, - afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)), + afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(async res => + setTimeout( + async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), // + didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0 + ) + ), }); }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { @@ -588,11 +638,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) { - UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } - } else if (this.onClickHandler?.script && !isScriptBox()) { + } 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 = () => @@ -621,11 +671,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clickFunc(); }, 350); } else clickFunc(); - } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + } 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, this.props, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { - // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + // 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 } else { runInAction(() => (this._pendingDoubleClick = true)); @@ -656,6 +707,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } return; } + this._cursorTimer = setTimeout( + action(() => { + this._cursorPress = true; + this.props.select(false); + }), + 1000 // long press required duration + ); this._downX = e.clientX; this._downY = e.clientY; if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { @@ -681,6 +739,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; + @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; @@ -690,7 +749,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.dropAction || undefined) as dropActionType)); + 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 @@ -704,8 +765,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps document.removeEventListener('pointerup', this.onPointerUp); }; + @action onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); + this._longPress = this._cursorPress; + this._cursorTimer && clearTimeout(this._cursorTimer); + this._cursorPress = false; if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); @@ -721,8 +786,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => { this.Document.ignoreClick = false; if (setPushpin) { - this.Document.isPushpin = !this.Document.isPushpin; - this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton; + this.Document.followLinkToggle = !this.Document.followLinkToggle; + this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton; } else { this.Document._isLinkButton = !this.Document._isLinkButton; } @@ -739,14 +804,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = true; + this.Document.followLinkToggle = true; }; @undoBatch @action followLinkOnClick = (location: Opt<string>, zoom: boolean): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; }; @@ -755,7 +820,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps selectOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = false; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.onClick = this.layoutDoc.onClick = undefined; }; @undoBatch @@ -803,7 +868,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }); DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from'); } - this.Document.followLinkLocation = 'inPlace'; + this.Document.followLinkLocation = OpenWhere.inPlace; this.Document.followLinkZoom = true; this.Document._isLinkButton = true; }; @@ -834,14 +899,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; if (e && !(e.nativeEvent as any).dash) { - const onDisplay = () => - setTimeout(() => { - DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.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. - setTimeout(() => { - const ele = document.elementFromPoint(e.clientX, e.clientY); - simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); - }); - }); + const onDisplay = () => { + DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.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. + setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); + }; if (navigator.userAgent.includes('Macintosh')) { cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); } else { @@ -862,7 +923,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); 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, 'add:right'), icon: 'eye' }); + !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', @@ -915,10 +976,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !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 ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', true, false), icon: 'link' }); + onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' }); !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' }); onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' }); - onClicks.push({ description: (this.Document.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' }); + onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' }); 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' }); } else if (DocListCast(this.Document.links).length) { @@ -962,8 +1023,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } 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 }), 'add:right'), 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 }), 'add:right'), icon: 'keyboard' }); + 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' }); @@ -1046,34 +1107,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @observable _retryThumb = 1; thumbShown = () => { + const childHighlighted = () => + Array.from(Doc.highlightedDocs.keys()) + .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() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) && + ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) && !this._componentView?.isAnyChildContentActive?.() ? true : false; }; - linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale; - @computed get contents() { - TraceMobx(); + 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 audioView = - (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : ( - <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> - <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> - <FontAwesomeIcon - className="documentView-audioFont" - style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }} - icon={!audioAnnosCount ? 'microphone' : 'file-audio'} - size="sm" - /> - </div> - </Tooltip> - ); - + 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> + ); + } + @computed get contents() { + TraceMobx(); return ( <div className="documentView-contentsView" @@ -1095,10 +1163,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps width={this.props.PanelWidth()} height={this.props.PanelHeight()} onError={(e: any) => { - setTimeout( - action(() => (this._retryThumb = 0)), - 0 - ); + setTimeout(action(() => (this._retryThumb = 0))); setTimeout( action(() => (this._retryThumb = 1)), 150 @@ -1111,7 +1176,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {...this.props} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} - isHovering={this.isHovering} + isHovering={this.props.isHovering} setContentView={this.setContentView} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} @@ -1124,14 +1189,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} - {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : ( - <DocumentLinksButton - View={this.props.DocumentView()} - scaling={this.linkButtonInverseScaling} - Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -32]} - /> - )} - {audioView} + {this.audioAnnoView} </div> ); } @@ -1147,16 +1205,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { - switch (property) { - case StyleProp.ShowTitle: - return ''; - case StyleProp.PointerEvents: - return 'none'; - case StyleProp.LinkSource: - return this.props.Document; // pass the LinkSource to the LinkAnchorBox - default: - return this.props.styleProvider?.(doc, props, property); + // prettier-ignore + 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 } + 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 'unrendered' with their 'annotationOn' set to this document). e.g., @@ -1183,7 +1238,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); return filtered.map((link, i) => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -1210,7 +1265,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const self = this; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos.lastElement(); - if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) { + if (anno instanceof AudioField && this.audioAnnoState === 'stopped') { new Howl({ src: [anno.url.href], format: ['mp3'], @@ -1218,16 +1273,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps loop: false, volume: 0.5, onend: function () { - runInAction(() => { - self.dataDoc.audioAnnoState = 0; - }); + runInAction(() => (self.dataDoc.audioAnnoState = 'stopped')); }, }); - this.dataDoc.audioAnnoState = 1; + this.dataDoc.audioAnnoState = 'playing'; } }; - static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) { + static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { let gumStream: any; let recorder: any; navigator.mediaDevices @@ -1262,14 +1315,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => (dataDoc.audioAnnoState = 2)); + runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); - setTimeout(() => { + const stopFunc = () => { recorder.stop(); DictationManager.Controls.stop(false); - runInAction(() => (dataDoc.audioAnnoState = 0)); + runInAction(() => (dataDoc.audioAnnoState = 'stopped')); gumStream.getAudioTracks()[0].stop(); - }, 5000); + }; + if (onRecording) onRecording(stopFunc); + else setTimeout(stopFunc, 5000); }); } @@ -1329,7 +1384,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .join('\\')} display={'block'} fontSize={10} - GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} + GetValue={() => { + this.props.select(false); + return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; + }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { if (this.props.showTitle) { @@ -1360,10 +1418,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); } - isHovering = () => this._isHovering; - @observable _isHovering = false; @observable _: string = ''; - _hoverTimeout: any = undefined; renderDoc = (style: object) => { TraceMobx(); const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); @@ -1374,27 +1429,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className={`documentView-node${this.topMost ? '-topmost' : ''}`} id={this.props.Document[Id]} - onPointerEnter={action(() => { - clearTimeout(this._hoverTimeout); - this._isHovering = true; - })} - onPointerLeave={action(() => { - clearTimeout(this._hoverTimeout); - this._hoverTimeout = setTimeout( - action(() => (this._isHovering = false)), - 500 - ); - })} style={{ ...style, background: isButton || thumb ? undefined : this.backgroundColor, opacity: this.opacity, - cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', + 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'}`, + 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} @@ -1403,25 +1447,55 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ) ); }; + + /** + * 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?.presEffectDirection ?? presEffectDoc?.linkAnimDirection; + 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?.presTransition, 'number', null), + }; + //prettier-ignore + switch (StrCast(presEffectDoc?.presEffect, 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 <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; + } + } 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 boxShadow = this.props.treeViewDoc || !highlighting - ? null + ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, - outline: highlighting && !this.borderRounding && highlighting ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlighting && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, + 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, }); - const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc; + const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc); return ( <div className={`${DocumentView.ROOT_DIV} docView-hack`} @@ -1474,7 +1548,37 @@ export class DocumentView extends React.Component<DocumentViewProps> { return 'DocumentView(' + this.props.Document?.title + ')'; } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); + public ViewTimer: NodeJS.Timeout | undefined; // timer for res private _disposers: { [name: string]: IReactionDisposer } = {}; + public clearViewTransition = () => { + this.ViewTimer && clearTimeout(this.ViewTimer); + this.rootDoc._viewTransition = undefined; + }; + public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { + this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; + if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`; + this.ViewTimer && clearTimeout(this.ViewTimer); + return (this.ViewTimer = setTimeout(() => { + this.rootDoc._viewTransition = undefined; + this.rootDoc._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 + ); + } public static showBackLinks(linkSource: Doc) { const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; @@ -1535,6 +1639,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } + @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} />; + } + @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @@ -1576,7 +1690,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.PanelHeight(); } @computed get Xshift() { - return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; + return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; } @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) @@ -1593,10 +1707,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } - const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); + const xf = this.docView?.props + .ScreenToLocalTransform() + .scale(this.trueNativeWidth() ? this.nativeScaling : 1) + .inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); @@ -1605,15 +1722,21 @@ export class DocumentView extends React.Component<DocumentViewProps> { return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; }; - public iconify(finished?: () => void) { + public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); + const animTime = this.docView?._animateScaleTime; + runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime)); + const finalFinished = action(() => { + finished?.(); + this.docView && (this.docView._animateScaleTime = animTime); + }); const layoutKey = Cast(this.Document.layoutKey, 'string', null); if (layoutKey !== 'layout_icon') { - this.switchViews(true, 'icon', finished); + this.switchViews(true, 'icon', finalFinished); if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished); + this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); this.Document.deiconifyLayout = undefined; this.props.bringToFront(this.rootDoc); } @@ -1639,15 +1762,19 @@ export class DocumentView extends React.Component<DocumentViewProps> { this.docView && (this.docView._animateScalingTo = 0); finished?.(); }), - this.docView!._animateScaleTime - 10 + this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0 ); }), - this.docView!._animateScaleTime - 10 + 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; + } 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); @@ -1657,11 +1784,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelHeight = () => this.panelHeight; NativeDimScaling = () => this.nativeScaling; selfView = () => this; + trueNativeWidth = () => returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, false)); screenToLocalTransform = () => this.props .ScreenToLocalTransform() .translate(-this.centeringX, -this.centeringY) - .scale(1 / this.nativeScaling); + .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, @@ -1680,14 +1808,48 @@ 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> + ); + } render() { 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"> + <div + className="contentFittingDocumentView" + onPointerEnter={action(() => { + clearTimeout(this._hoverTimeout); + this._isHovering = true; + })} + onPointerLeave={action(() => { + clearTimeout(this._hoverTimeout); + this._hoverTimeout = setTimeout( + action(() => (this._isHovering = false)), + 500 + ); + })}> {!this.props.Document || !this.props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1712,12 +1874,16 @@ 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))} /> + {this.htmlOverlay} </div> )} + + {this.linkCountView} </div> ); } |