diff options
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 577 |
1 files changed, 287 insertions, 290 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 581d43b94..3191e04db 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,12 +1,16 @@ +/* eslint-disable no-use-before-define */ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Howl } from 'howler'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; -import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; +import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils'; +import { Utils, emptyFunction, emptyPath } from '../../../Utils'; +import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; +import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -16,22 +20,17 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; +import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; -import { Networking } from '../../Network'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocUtils, Docs } from '../../documents/Documents'; -import { DictationManager } from '../../util/DictationManager'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../util/DragManager'; +import { Docs } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; -import { FollowLinkScript } from '../../util/LinkFollower'; -import { LinkManager } from '../../util/LinkManager'; +import { UPDATE_SERVER_CACHE } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SearchUtil } from '../../util/SearchUtil'; -import { SelectionManager } from '../../util/SelectionManager'; -import { SettingsManager } from '../../util/SettingsManager'; -import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager, undoBatch, undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -39,53 +38,18 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent, ViewBoxInterface } from '../DocComponent'; import { EditableView } from '../EditableView'; import { FieldsDropdown } from '../FieldsDropdown'; -import { GestureOverlay } from '../GestureOverlay'; import { LightboxView } from '../LightboxView'; -import { AudioAnnoState, StyleProp } from '../StyleProvider'; +import { PinProps } from '../PinFuncs'; +import { StyleProp } from '../StyleProp'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps, FieldViewSharedProps } from './FieldView'; -import { KeyValueBox } from './KeyValueBox'; -import { LinkAnchorBox } from './LinkAnchorBox'; +import { FocusViewOptions } from './FocusViewOptions'; +import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; -interface Window { - MediaRecorder: MediaRecorder; -} -declare class MediaRecorder { - constructor(e: any); // whatever MediaRecorder has -} - -export enum OpenWhereMod { - none = '', - left = 'left', - right = 'right', - top = 'top', - bottom = 'bottom', - keyvalue = 'keyValue', -} -export enum OpenWhere { - lightbox = 'lightbox', - add = 'add', - addLeft = 'add:left', - addRight = 'add:right', - addBottom = 'add:bottom', - close = 'close', - toggle = 'toggle', - toggleRight = 'toggle:right', - replace = 'replace', - replaceRight = 'replace:right', - replaceLeft = 'replace:left', - inParent = 'inParent', - inParentFromScreen = 'inParentFromScreen', - overlay = 'overlay', - addRightKeyvalue = 'add:right:keyValue', -} -export function returnEmptyDocViewList() { - return [] as DocumentView[]; -} export interface DocumentViewProps extends FieldViewSharedProps { hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected @@ -112,17 +76,20 @@ export interface DocumentViewProps extends FieldViewSharedProps { dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; + + parent?: any; } @observer export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() { // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. + /** * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without * needing to know about/reference MainView */ - public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; + public static addDocTabFunc: (doc: Doc | Doc[], location: OpenWhere) => boolean = returnFalse; private _disposers: { [name: string]: IReactionDisposer } = {}; private _doubleClickTimeout: NodeJS.Timeout | undefined; @@ -146,7 +113,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @observable _mounted = false; // turn off all pointer events if component isn't yet mounted (enables nested Docs in alternate UI textboxes that appear on hover which otherwise would grab focus from the text box, reverting to the original UI ) @observable _isContentActive: boolean | undefined = undefined; @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined = undefined; - @observable _componentView: Opt<ViewBoxInterface> = undefined; // needs to be accessed from DocumentView wrapper class + @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; @@ -169,7 +136,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore @computed get onClickHandler() { - return this._props.onClickScript?.() ?? this._props.onBrowseClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick)); + return this._props.onClickScript?.() ?? ScriptCast(this.Document.onClick, ScriptCast(this.layoutDoc.onClick)); } @computed get onDoubleClickHandler() { return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick, ScriptCast(this.Document.onDoubleClick)); @@ -183,23 +150,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get disableClickScriptFunc() { const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; - // prettier-ignore return ( + // eslint-disable-next-line no-use-before-define DocumentView.LongPress || onScriptDisable === 'always' || (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.())) - ); + ); // prettier-ignore } @computed get _rootSelected() { return this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.()); } - /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive + /// disable pointer events on content when there's an enabled onClick script (and not in explore mode) and the contents aren't forced active, or if contents are marked inactive @computed get _contentPointerEvents() { TraceMobx(); return this._props.contentPointerEvents ?? ((!this.disableClickScriptFunc && // this.onClickHandler && - !this._props.onBrowseClickScript?.() && + !SnappingManager.ExploreMode && !this.layoutDoc.layout_isSvg && this.isContentActive() !== true) || this.isContentActive() === false) @@ -211,11 +178,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link - // - and links to PDF/Web docs at a certain scroll location never create an explicit view. - // For each of these, we create LinkAnchorBox's on the border of the DocumentView. + // - and links to PDF/Web docs at a certain scroll location never create an explicit anchor view. @computed get directLinks() { TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.Document).filter( + return Doc.Links(this.Document).filter( link => (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) || (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) || @@ -225,11 +191,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } @computed get _allLinks(): Doc[] { TraceMobx(); - return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); + return Doc.Links(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); } @computed get filteredLinks() { - return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []); } componentWillUnmount() { @@ -237,7 +203,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } componentDidMount() { - runInAction(() => (this._mounted = true)); + runInAction(() => { + this._mounted = true; + }); this.setupHandlers(); this._disposers.contentActive = reaction( () => @@ -249,19 +217,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() ? true : undefined, - active => (this._isContentActive = active), + active => { + this._isContentActive = active; + }, { fireImmediately: true } ); this._disposers.pointerevents = reaction( () => this.style(this.Document, StyleProp.PointerEvents), - pointerevents => (this._pointerEvents = pointerevents), + pointerevents => { + this._pointerEvents = pointerevents; + }, { fireImmediately: true } ); } preDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData && this.isContentActive() && !this.props.dontRegisterView) { - dragData.dropAction = dropAction ? dropAction : dragData.dropAction; + dragData.dropAction = dropAction || dragData.dropAction; e.stopPropagation(); } }; @@ -279,10 +251,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { - //console.log("docview drag") const docView = this._docView; if (this._mainCont.current && docView) { - const views = SelectionManager.Views.filter(dv => dv.ContentDiv); + const views = DocumentView.Selected().filter(dv => dv.ContentDiv); const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [docView]; const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document)); const screenXf = docView.screenToViewTransform(); @@ -291,8 +262,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document dragData.dropAction = dropAction; dragData.removeDocument = this._props.removeDocument; dragData.moveDocument = this._props.moveDocument; - dragData.draggedViews = [docView]; - dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false; + dragData.dragEnding = () => docView.props.dragEnding?.(); + dragData.dragStarting = () => docView.props.dragStarting?.(); + dragData.canEmbed = !!(this.Document.dragAction ?? this._props.dragAction); (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData); DragManager.StartDocumentDrag( selected.map(dv => dv.ContentDiv!), @@ -309,10 +281,25 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title'; setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined }; + onBrowseClick = (e: React.MouseEvent) => { + const browseTransitionTime = 500; + DocumentView.DeselectAll(); + DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime }; + if (!focused && this._docView) { + this._docView + .docViewPath() + .reverse() + .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options)); + Doc.linkFollowHighlight(this.Document, false); + } + }); + e.stopPropagation(); + }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; const documentView = this._docView; - if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document); @@ -334,7 +321,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title); } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') { UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap'); - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); Doc.UnBrushDoc(this.Document); } else { this._singleClickFunc?.(); @@ -369,6 +356,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); + // eslint-disable-next-line no-use-before-define } else if (!DocumentView.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; @@ -381,8 +369,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerDown = (e: React.PointerEvent): void => { if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; - this._longPressSelector = setTimeout(() => (DocumentView.LongPress && this._props.select(false), 1000)); //!!! - if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView; + // eslint-disable-next-line no-use-before-define + this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000); + if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView; this._downX = e.clientX; this._downY = e.clientY; @@ -390,10 +379,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if ((Doc.ActiveTool === InkTool.None || this._props.addDocTab === returnFalse) && !(this._props.TemplateDataDocument && !(e.ctrlKey || e.button > 0))) { // 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 ( - // prettier-ignore - (this._props.isDocumentActive?.() || this._props.isContentActive?.()) && - !this._props.onBrowseClickScript?.() && + if ((this._props.isDocumentActive?.() || this._props.isContentActive?.()) && + !SnappingManager.ExploreMode && !this.Document.ignoreClick && e.button === 0 && !Doc.IsInMyOverlay(this.layoutDoc) @@ -405,7 +392,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } - } + } // prettier-ignore document.addEventListener('pointerup', this.onPointerUp); } }; @@ -413,7 +400,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerMove = (e: PointerEvent): void => { 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())) { + if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); @@ -431,14 +418,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this.onPointerUpHandler?.script) { this.onPointerUpHandler.script.run({ this: this.Document }, console.log); - } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { - this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME; + } else if (e.button === 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + this._doubleTap = (this.onDoubleClickHandler?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < ClientUtils.CLICK_TIME; if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } + // eslint-disable-next-line no-use-before-define if (DocumentView.LongPress) e.preventDefault(); }; - toggleFollowLink = undoable((zoom?: boolean, setTargetToggle?: boolean): void => { + toggleFollowLink = undoable((): void => { const hadOnClick = this.Document.onClick; this.noOnClick(); this.Document.onClick = hadOnClick ? undefined : FollowLinkScript(); @@ -459,19 +447,17 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }, 'default on click'); deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); - setToggleDetail = undoable( - (scriptFieldKey: 'onClick') => - (this.Document[scriptFieldKey] = ScriptField.MakeScript( - `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) - .replace('layout_', '') - .replace(/^layout$/, 'detail')}")`, - { documentView: 'any' } - )), - 'set toggle detail' - ); + setToggleDetail = undoable((scriptFieldKey: 'onClick') => { + this.Document[scriptFieldKey] = ScriptField.MakeScript( + `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) + .replace('layout_', '') + .replace(/^layout$/, 'detail')}")`, + { documentView: 'any' } + ); + }, 'set toggle detail'); drop = undoable((e: Event, de: DragManager.DropEvent) => { - if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; + if (this._props.dontRegisterView) return false; if (this.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); @@ -492,7 +478,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (linkDoc) { de.complete.linkDocument = linkDoc; linkDoc.layout_isSvg = true; - DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc); + DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc); } } e.stopPropagation(); @@ -506,7 +492,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; - input.onchange = _e => { + input.onchange = () => { if (input.files) { const batch = UndoManager.StartBatch('importing'); Doc.importDocument(input.files[0]).then(doc => { @@ -524,7 +510,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); - //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false); + // !this._props.isSelected(true) && DocumentView.SelectView(this.DocumentView(), false); } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { @@ -536,7 +522,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document e.stopPropagation(); e.persist(); - if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { + if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - (e?.clientX ?? 0)) > 3 || Math.abs(this._downY - (e?.clientY ?? 0)) > 3)) { return; } } @@ -584,11 +570,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this._props.bringToFront) { const zorders = cm.findByDescription('ZOrder...'); const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : []; - zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' }); - zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' }); + zorderItems.push({ description: 'Bring to Front', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' }); + zorderItems.push({ description: 'Send to Back', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' }); zorderItems.push({ description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', - event: undoBatch(action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged))), + event: undoBatch( + action(() => { + this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged; + }) + ), icon: 'hand-point-up', }); !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' }); @@ -598,14 +588,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: 'Enter Portal', event: undoable(e => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' }); + onClicks.push({ description: 'Enter Portal', event: undoable(() => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); if (!this.Document.annotationOn) { onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); - } else if (LinkManager.Links(this.Document).length) { + } else if (Doc.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', 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' }); @@ -614,9 +604,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const funcs: ContextMenuProps[] = []; if (!Doc.noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) }); - funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) }); + funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')); } }); // prettier-ignore + funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => { this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')); } }); // prettier-ignore + funcs.push({ description: 'Drag Document', icon: 'edit', event: () => { this.layoutDoc.onDragStart = undefined; } }); // prettier-ignore cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' }); } @@ -625,14 +615,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!Doc.IsSystem(this.Document)) { if (!Doc.noviceMode) { moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.Document, this._props.TemplateDataDocument), icon: 'concierge-bell' }); - moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' }); - - if (Cast(Doc.GetProto(this.Document).data, listSpec(Doc))) { - moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.Document }).then(console.log), icon: 'caret-square-right' }); - moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.Document), icon: 'caret-square-right' }); - moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.Document), icon: 'caret-square-right' }); - } - moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' }); + moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => { this.Document._chromeHidden = !this.Document._chromeHidden; }, icon: 'project-diagram' }); // prettier-ignore + moreItems.push({ description: 'Copy ID', event: () => ClientUtils.CopyText(Doc.globalServerPath(this.Document)), icon: 'fingerprint' }); } } @@ -640,8 +624,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } const constantItems: ContextMenuProps[] = []; if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) { - constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.Document) }); - (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' }); + constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => DocUtils.Zip(this.Document) }); + constantItems.push({ description: 'Share', event: () => DocumentView.ShareOpen(this._docView), icon: 'users' }); if (this._props.removeDocument && Doc.ActiveDashboard !== this.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); @@ -656,8 +640,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.dataDoc), icon: 'hand-point-right' }); - let documentationDescription: string | undefined = undefined; - let documentationLink: string | undefined = undefined; + let documentationDescription: string | undefined; + let documentationLink: string | undefined; switch (this.Document.type) { case DocumentType.COL: documentationDescription = 'See collection documentation'; @@ -691,6 +675,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document documentationDescription = 'See DataViz node documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/'; break; + default: } // Add link to help documentation (unless the doc contents have been overriden in which case the documentation isn't relevant) if (!this.docContents && documentationDescription && documentationLink) { @@ -711,8 +696,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document panelHeight = () => this._props.PanelHeight() - this.headerMargin; screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler; - setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); - setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); + setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore + setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -727,44 +712,18 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document case StyleProp.PointerEvents: return 'none'; case StyleProp.Highlighting: return undefined; case StyleProp.Opacity: { - const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []); return filtered.some(link => link._link_displayArrow) ? 0 : undefined; } + default: } return this._props.styleProvider?.(doc, props, property); }; - removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false); - @computed get allLinkEndpoints() { - // the small blue dots that mark the endpoints of links - if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; - return this.filteredLinks.map(link => ( - <div className="documentView-anchorCont" key={link[Id]}> - <DocumentView - {...this._props} - isContentActive={returnFalse} - Document={link} - containerViewPath={this._props.docViewPath} - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - dontRegisterView={false} - layout_showTitle={returnEmptyString} - hideCaptions={true} - hideLinkAnchors={true} - layout_fitWidth={returnTrue} - removeDocument={this.removeLinkByHiding(link)} - styleProvider={this.anchorStyleProvider} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)} - /> - </div> - )); - } - @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; - const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return ( <div className="documentView-contentsView" @@ -787,38 +746,35 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document setTitleFocus={this.setTitleFocus} hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); - fieldsDropdown = (placeholder: string) => { - return ( - <div - ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} - onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> - <FieldsDropdown - Document={this.Document} - placeholder={placeholder} - selectFunc={action((field: string | number) => { - if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = field; - } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = field; - } - this._changingTitleField = false; - })} - menuClose={action(() => (this._changingTitleField = false))} - /> - </div> - ); - }; + fieldsDropdown = (placeholder: string) => ( + <div + ref={action((r: any) => { r && (this._titleDropDownInnerWidth = DivWidth(r));} )} // prettier-ignore + onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore + style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> + <FieldsDropdown + Document={this.Document} + placeholder={placeholder} + selectFunc={action((field: string | number) => { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + })} + menuClose={action(() => { this._changingTitleField = false; })} // prettier-ignore + /> + </div> + ); /** * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by * setting layout_showTitle using the format: field1[:hover] - **/ + * */ @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); @@ -826,7 +782,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.Document; const background = StrCast( this.layoutDoc.layout_headingColor, - StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)) + // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, + StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); @@ -840,7 +798,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, width: 100 - sidebarWidthPercent + '%', - color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), + color: background === 'transparent' ? SnappingManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> @@ -861,11 +819,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document contents={ showTitle .split(';') - .map(field => Field.toJavascriptString(this.Document[field] as Field)) + .map(field => Field.toJavascriptString(this.Document[field] as FieldType)) .join(' \\ ') || '-unset-' } display="block" - oneLine={true} + oneLine fontSize={(this.titleHeight / 15) * 10} GetValue={() => showTitle @@ -881,7 +839,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title'; } } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') { - KeyValueBox.SetField(targetDoc, showTitle, input); + Doc.SetField(targetDoc, showTitle, input); } return true; })} @@ -906,10 +864,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document xPadding={10} fieldKey={this.layout_showCaption} styleProvider={this.captionStyleProvider} - dontRegisterView={true} + dontRegisterView rootSelected={this.rootSelected} - noSidebar={true} - dontScale={true} + noSidebar + dontScale renderDepth={this._props.renderDepth} isContentActive={this.isContentActive} /> @@ -953,8 +911,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document render() { TraceMobx(); - const highlighting = this.highlighting; - const borderPath = this.borderPath; + const { highlighting, borderPath } = this; const boxShadow = !highlighting ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' @@ -969,23 +926,22 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }); return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events <div className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} - onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} - onPointerOver={e => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} + onClick={SnappingManager.ExploreMode ? this.onBrowseClick : this.onClick} + onPointerEnter={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} + onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)} style={{ borderRadius: this.borderRounding, pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> - <> - {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} - {borderPath?.jsx} - </> + {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])} + {borderPath?.jsx} </div> ); } @@ -995,7 +951,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * @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) { + public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc> /* , root: Doc */) { const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection; const effectProps = { left: dir === PresEffectDirection.Left, @@ -1006,10 +962,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document delay: 0, duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), }; - //prettier-ignore + // prettier-ignore switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { - default: - case PresEffect.None: return renderDoc; case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; @@ -1017,53 +971,52 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>; + case PresEffect.None: + default: return renderDoc; } } - public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { - let gumStream: any; - let recorder: any; - navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { - let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); - if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); - DictationManager.Controls.listen({ - interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), - continuous: { indefinite: false }, - }).then(results => { - if (results && [DictationManager.Controls.Infringed].includes(results)) { - DictationManager.Controls.stop(); - } - onEnd?.(); - }); - - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); - if (!(result instanceof Error)) { - const audioField = new AudioField(result.accessPaths.agnostic.client); - const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); - if (audioAnnos) audioAnnos.push(audioField); - else dataDoc[field + '_audioAnnotations'] = new List([audioField]); - } - }; - recorder.start(); - const stopFunc = () => { - recorder.stop(); - DictationManager.Controls.stop(false); - dataDoc.audioAnnoState = AudioAnnoState.stopped; - gumStream.getAudioTracks()[0].stop(); - }; - if (onRecording) onRecording(stopFunc); - else setTimeout(stopFunc, 5000); - }); - } } @observer export class DocumentView extends DocComponent<DocumentViewProps>() { public static ROOT_DIV = 'documentView-effectsWrapper'; - public get displayName() { return 'DocumentView(' + this.Document?.title + ')'; } // prettier-ignore + // Sharing Manager + public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void; + // LinkFollower + public static FollowLink: (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => boolean; + // selection funcs + public static DeselectAll: (except?: Doc) => void | undefined; + public static DeselectView: (dv: DocumentView | undefined) => void | undefined; + public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined; + public static Selected: () => DocumentView[]; + public static SelectedDocs: () => Doc[]; + public static SelectSchemaDoc: (doc: Doc, deselectAllFirst?: boolean) => void; + public static SelectedSchemaDoc: () => Opt<Doc>; + // view mgr funcs + public static activateTabView: (tabDoc: Doc) => boolean; + public static allViews: () => DocumentView[]; + public static addView: (dv: DocumentView) => void | undefined; + public static removeView: (dv: DocumentView) => void | undefined; + public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean; + public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined; + public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>; + public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[]; + public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>; + public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>; + public static showDocument: ( + targetDoc: Doc, // document to display + optionsIn: FocusViewOptions, // options for how to navigate to target + finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. + ) => Promise<void>; + public static linkCommonAncestor: (link: Doc) => DocumentView | undefined; + // pin func + public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void; + // gesture + public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to. + // media playing + @observable public static CurrentlyPlaying: DocumentView[] = []; + + public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore public ContentRef = React.createRef<HTMLDivElement>(); private _htmlOverlayEffect: Opt<Doc>; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -1081,9 +1034,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]); - @computed public static get exploreMode() { - return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); - } constructor(props: DocumentViewProps) { super(props); @@ -1101,7 +1051,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable public static LongPress = false; @computed private get shouldNotScale() { - return (this.layout_fitWidth && !this.nativeWidth) || this._props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); + return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.(); } @computed private get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1152,17 +1102,17 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { componentDidMount() { runInAction(() => this.Document[DocViews].add(this)); this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction); - !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this); + !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.addView(this); } componentWillUnmount() { this._viewTimer && clearTimeout(this._viewTimer); runInAction(() => this.Document[DocViews].delete(this)); Object.values(this._disposers).forEach(disposer => disposer?.()); - !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); + !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this); } - public set IsSelected(val) { runInAction(() => (this._selected = val)); } // prettier-ignore + public set IsSelected(val) { runInAction(() => { this._selected = val; }); } // prettier-ignore public get IsSelected() { return this._selected; } // prettier-ignore public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore @@ -1177,7 +1127,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { return this._props.layout_fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth; } @computed get anchorViewDoc() { - return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; + return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined; } @computed get getBounds(): Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }> { @@ -1190,20 +1140,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); - if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined }; - } // transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.()); return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() }; } @computed get nativeWidth() { - return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); + return returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed get nativeHeight() { - return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); + return returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed public get centeringX() { return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } // prettier-ignore @computed public get centeringY() { return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } // prettier-ignore @@ -1212,8 +1158,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { * path of DocumentViews hat contains this DocumentView (does not includes this DocumentView thouhg) */ public get containerViewPath() { return this._props.containerViewPath; } // prettier-ignore - public get CollectionFreeFormView() { return this.CollectionFreeFormDocumentView?.CollectionFreeFormView; } // prettier-ignore - public get CollectionFreeFormDocumentView() { return this._props.CollectionFreeFormDocumentView?.(); } // prettier-ignore + public get LocalRotation() { return this._props.LocalRotation?.(); } // prettier-ignore public clearViewTransition = () => { this._viewTimer && clearTimeout(this._viewTimer); @@ -1232,18 +1177,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); const animTime = this._docViewInternal?.animateScaleTime(); - runInAction(() => this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime)); + runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore const finalFinished = action(() => { finished?.(); this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); }); - const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null); - if (layout_fieldKey !== 'layout_icon') { + const layoutFieldKey = Cast(this.Document.layout_fieldKey, 'string', null); + if (layoutFieldKey !== 'layout_icon') { this.switchViews(true, 'icon', finalFinished); - if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', ''); + if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true); + this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } @@ -1263,7 +1208,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => (self.dataDoc.audioAnnoState = AudioAnnoState.stopped)), + onend: action(() => { self.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore }); this.dataDoc.audioAnnoState = AudioAnnoState.playing; break; @@ -1271,6 +1216,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.dataDoc[AudioPlay]?.stop(); this.dataDoc.audioAnnoState = AudioAnnoState.stopped; break; + default: } } }; @@ -1285,10 +1231,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this._docViewInternal._animateScaleTime = time; } }); - public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { + public setAnimEffect = (presEffect: Doc, timeInMs: number /* , afterTrans?: () => void */) => { this._animEffectTimer && clearTimeout(this._animEffectTimer); this.Document[Animation] = presEffect; - this._animEffectTimer = setTimeout(() => (this.Document[Animation] = undefined), timeInMs); + this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore }; public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans); @@ -1303,9 +1249,9 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (checkResult) { return Doc.UserDoc().defaultTextLayout; } - const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined; + const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; undoable(() => { - var tempDoc: Opt<Doc>; + let tempDoc: Opt<Doc>; if (view) { if (!view.layoutDoc.isTemplateDoc) { tempDoc = view.Document; @@ -1323,6 +1269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; }, 'set default template')(); + return undefined; } /** @@ -1336,12 +1283,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', ''); if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout; const defaultLayout = StrCast(this.Document.layout_default); - if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); + if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true); else this.switchViews(true, detailLayoutKeySuffix, undefined, true); }; public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { const batch = UndoManager.StartBatch('switchView:' + view); - runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc + // shrink doc first.. + runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1349,7 +1297,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } else { this.setCustomView(custom, view); } - this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // expand it + this._docViewInternal && (this._docViewInternal._animateScalingTo = 1); // now expand it setTimeout( action(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0); @@ -1367,20 +1315,20 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { */ public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]); - layout_fitWidthFunc = (doc: Doc) => BoolCast(this.layout_fitWidth); + layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth); screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale; isSelected = () => this.IsSelected; select = (extendSelection: boolean, focusSelection?: boolean) => { - /*if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this); - else {*/ - SelectionManager.SelectView(this, extendSelection); - if (focusSelection) { - DocumentManager.Instance.showDocument(this.Document, { - willZoomCentered: true, - zoomScale: 0.9, - zoomTime: 500, - }); - } + // if (this.IsSelected && DocumentView.Selected().length > 1) DocumentView.DeselectView(this); + // else { + DocumentView.SelectView(this, extendSelection); + if (focusSelection) { + DocumentView.showDocument(this.Document, { + willZoomCentered: true, + zoomScale: 0.9, + zoomTime: 500, + }); + } //} }; backgroundColor = () => this._docViewInternal?.backgroundBoxColor; @@ -1391,7 +1339,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; NativeDimScaling = () => this.nativeScaling; - hideLinkCount = () => (this.hideLinkButton ? true : false); + hideLinkCount = () => !!this.hideLinkButton; selfView = () => this; /** * @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView) @@ -1414,17 +1362,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ref={r => { const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition if (r && val !== this._enableHtmlOverlayTransitions) { - setTimeout(action(() => (this._enableHtmlOverlayTransitions = val))); + setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore } }} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}> <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}> {DocumentViewInternal.AnimationEffect( <div className="webBox-textHighlight"> - <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} /> + <ObserverJsxParser autoCloseVoidElements key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} /> </div>, - { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc, - this.Document + { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc )} </div> </div> @@ -1437,7 +1384,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div + id={this.ViewGuid} + className="contentFittingDocumentView" + onPointerEnter={action(() => { + this._isHovering = true; + })} + onPointerLeave={action(() => { + this._isHovering = false; + })}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1463,14 +1418,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { layout_fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToContentsTransform} focus={this._props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} + ref={action((r: DocumentViewInternal | null) => { + r && (this._docViewInternal = r); + })} /> {this.htmlOverlay()} {this.ComponentView?.infoUI?.()} </div> )} {/* display link count button */} - <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover={true} Bottom={this.topMost} ShowCount={true} /> + <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.screenToLocalScale} OnHover Bottom={this.topMost} ShowCount /> </div> ); } @@ -1494,7 +1451,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { // shows a stacking view collection (by default, but the user can change) of all documents linked to the source public static showBackLinks(linkAnchor: Doc) { - const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; + const docId = ClientUtils.CurrentUserEmail() + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; // prettier-ignore DocServer.GetRefField(docId).then(docx => LightboxView.Instance.SetLightboxDoc( @@ -1505,26 +1462,65 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } } +export function returnEmptyDocViewList() { + return emptyPath; +} + +// eslint-disable-next-line default-param-last +export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { + let doc = docIn; + const options = optionsIn; + const func = () => { + const cv = DocumentView.getDocumentView(containingDoc); + const dv = DocumentView.getDocumentView(doc, cv); + if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { + DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); + } else { + const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); + const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; + options.toggleTarget = undefined; + DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => { + const cvFound = DocumentView.getDocumentView(containingDoc); + const dvFound = DocumentView.getDocumentView(doc, cvFound); + dvFound && Doc.linkFollowHighlight(dvFound.Document); + }); + } + }; + if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) { + doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!; + } + if (doc.hidden) { + doc.hidden = false; + options.toggleTarget = false; + setTimeout(func); + } else func(); +} +ScriptingGlobals.add(DocFocusOrOpen); + +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { documentView.iconify(); documentView.select(false); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) { - LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); + LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { dv.toggleDetail(detailLayoutKeySuffix); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { const collectedLinks = DocListCast(linkCollection[DocData].data); let wid = NumCast(linkSource._width); let embedding: Doc | undefined; - const links = LinkManager.Links(linkSource); + const links = Doc.Links(linkSource); links.forEach(link => { - const other = LinkManager.getOppositeAnchor(link, linkSource); + const other = Doc.getOppositeAnchor(link, linkSource); const otherdoc = DocCast(other?.annotationOn ?? other); if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { embedding = Doc.MakeEmbedding(otherdoc); @@ -1535,9 +1531,10 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding); } }); - embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise + embedding && UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise return links; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { const tag = StrCast(collection.title).split('-->')[1]; const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys()); |