diff options
| author | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-18 16:31:21 -0500 |
|---|---|---|
| committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-18 16:31:21 -0500 |
| commit | ec18ae95005b9adc674a1ed6c3e976aa987ed7d2 (patch) | |
| tree | ae991bbfad86ca6230a21c86b40c274e2cfe6607 /src/client/views/nodes | |
| parent | 546540013de0a7cb647f30f1fcb513ce52048b72 (diff) | |
| parent | 0d59f6bc23c755c4eab2503add28699f5a5b1992 (diff) | |
merging
Diffstat (limited to 'src/client/views/nodes')
19 files changed, 316 insertions, 246 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index e24a671d0..ecc93ce67 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -89,7 +89,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } getAnchor = () => { - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "audioStart", "audioEnd", this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc; } componentWillUnmount() { @@ -354,9 +354,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} renderDepth={this.props.renderDepth + 1} - startTag={"audioStart"} - endTag={"audioEnd"} - focus={emptyFunction} + startTag={"_timecodeToShow" /* audioStart */} + endTag={"_timecodeToHide" /* audioEnd */} + focus={DocUtils.DefaultFocus} bringToFront={emptyFunction} CollectionView={undefined} duration={this.duration} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 4b0422ed3..e93d7ff72 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -153,7 +153,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF panelWidth = () => (this.sizeProvider?.width || this.props.PanelWidth?.()); panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.()); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); - focusDoc = (doc: Doc) => this.props.focus(doc, false); + focusDoc = (doc: Doc) => this.props.focus(doc); returnThis = () => this; render() { TraceMobx(); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 4b4720d58..7a8eb5628 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -166,9 +166,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo return { props: list }; } - componentWillUpdate(oldProps: any, newState: any) { - // console.log("willupdate", oldProps, this.props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed - } + // componentWillUpdate(oldProps: any, newState: any) { + // // console.log("willupdate", oldProps, this.props); // bcz: if you get a message saying something invalidated because reactive props changed, then this method allows you to figure out which prop changed + // } @computed get renderData() { TraceMobx(); diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 13a6c9df8..8a90d5d62 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, WidthSym, DocListCastAsync } from "../../../fields/Doc"; import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils"; import { TraceMobx } from "../../../fields/util"; import { DocUtils, Docs } from "../../documents/Documents"; @@ -17,6 +17,9 @@ import { Id } from "../../../fields/FieldSymbols"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); import './DocumentLinksButton.scss'; +import { DocServer } from "../../DocServer"; +import { LightboxView } from "../LightboxView"; +import { cat } from "shelljs"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -83,7 +86,35 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLinkView = this.props.View; } } else if (!this.props.InMenu) { - DocumentLinksButton.LinkEditorDocView = this.props.View; + if (doubleTap) { + const rootDoc = this.props.View.rootDoc; + const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish"; + DocServer.GetRefField(docid).then(async docx => { + const rootAlias = () => { + const rootAlias = Doc.MakeAlias(rootDoc); + rootAlias.x = rootAlias.y = 0; + return rootAlias; + } + let wid = rootDoc[WidthSym](); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid); + const docs = await DocListCastAsync(Doc.GetProto(target).data); + if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target); + DocListCast(rootDoc.links).forEach(link => { + const other = LinkManager.getOppositeAnchor(link, rootDoc); + const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) { + const alias = Doc.MakeAlias(otherdoc); + alias.x = wid; + alias.y = 0; + alias.lockedPosition = false; + wid += otherdoc[WidthSym](); + Doc.AddDocToList(Doc.GetProto(target), "data", alias); + } + }); + LightboxView.SetLightboxDoc(target); + }); + } + else DocumentLinksButton.LinkEditorDocView = this.props.View; } })); } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index e01a21530..36572b861 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -48,6 +48,25 @@ } } + .documentView-audioBackground { + display: inline-block; + width: 10%; + position: absolute; + top: 0px; + left: 0px; + border-radius: 25px; + background: white; + opacity: 0.3; + + svg { + width: 90% !important; + height: 70%; + position: absolute; + left: 5%; + top: 15%; + } + } + .documentView-treeView { max-height: 1.5em; text-overflow: ellipsis; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2c418e499..7fa790768 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,18 +1,22 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, HeightSym } from "../../../fields/Doc"; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { AudioField } from "../../../fields/URLField"; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { emptyFunction, hasDescendantTarget, OmitKeys, returnFalse, returnVal, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from "../../Network"; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; @@ -37,30 +41,54 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import "./DocumentView.scss"; import { FieldViewProps } from "./FieldView"; import { LinkAnchorBox } from './LinkAnchorBox'; +import { LinkDocPreview } from "./LinkDocPreview"; import { PresBox } from './PresBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); -import { List } from '../../../fields/List'; -import { Tooltip } from '@material-ui/core'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { LinkDocPreview } from "./LinkDocPreview"; +import { ObjectField } from "../../../fields/ObjectField"; +const { Howl } = require('howler'); + +interface Window { + MediaRecorder: MediaRecorder; +} + +declare class MediaRecorder { + // whatever MediaRecorder has + constructor(e: any); +} + +export enum ViewAdjustment { + resetView = 1, + doNothing = 0 +} + +export const ViewSpecPrefix = "_VIEW"; // 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 type DocAfterFocusFunc = (notFocused: boolean) => Promise<boolean>; -export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => void; +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 + 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 +} +export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>; +export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void; export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any; export interface DocComponentView { - getAnchor: () => Doc; - scrollFocus?: (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => void; + getAnchor?: () => Doc; + scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus + setViewSpec?: (anchor: Doc, preview: boolean) => void; back?: () => boolean; forward?: () => boolean; url?: () => string; submitURL?: (url: string) => boolean; + freeformData?: (force?: boolean) => Opt<{ panX: number, panY: number, scale: number }>; } export interface DocumentViewSharedProps { renderDepth: number; Document: Doc; DataDoc?: Doc; - fitContentsToDoc?: boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document + fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; setContentView?: (view: DocComponentView) => any; @@ -125,6 +153,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps, Document>(Document) { @observable _animateScalingTo = 0; + @observable _audioState = 0; private _downX: number = 0; private _downY: number = 0; private _firstX: number = -1; @@ -136,8 +165,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _timeout: NodeJS.Timeout | undefined; private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; - _componentView: Opt<DocComponentView>; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + _componentView: Opt<DocComponentView>; private get topMost() { return this.props.renderDepth === 0; } private get active() { return this.props.isSelected(true) || this.props.parentActive(true); } @@ -155,6 +184,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); } @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); } @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } + @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); } @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } @computed get nativeWidth() { return this.props.NativeWidth(); } @@ -378,11 +408,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } - focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { - if (this._componentView?.scrollFocus) { - return this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo, afterFocus); // bcz: smooth parameter should really be passed into focus() instead of inferred here - } - return this.props.focus(doc, willZoom, scale, afterFocus, docTransform); + focus = (anchor: Doc, options?: DocFocusOptions) => { + // 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 + setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); + const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing; + this.props.focus(options?.docTransform ? anchor : this.rootDoc, { + ...options, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0)) + }); + } onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && @@ -390,34 +427,29 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps let stopPropagate = true; let preventDefault = true; !StrListCast(this.props.Document.layers).includes(StyleLayers.Background) && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); - if (this._doubleTap && ((this.props.renderDepth && this.props.Document.type !== DocumentType.FONTICON) || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click if (this._timeout) { clearTimeout(this._timeout); this._timeout = undefined; } - if (!(e.nativeEvent as any).formattedHandled) { - if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const func = () => this.onDoubleClickHandler.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX: e.clientX, - clientY: e.clientY, - shiftKey: e.shiftKey - }, console.log); - UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); - } else if (!Doc.IsSystem(this.props.Document)) { - if (this.props.Document.type !== DocumentType.LABEL) { - UndoManager.RunInBatch(() => { - const fullScreenDoc = Cast(this.props.Document._fullScreenView, Doc, null) || this.props.Document; - this.props.addDocTab(fullScreenDoc, "lightbox"); - }, "double tap"); - SelectionManager.DeselectAll(); - } - Doc.UnBrushDoc(this.props.Document); + if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + const func = () => this.onDoubleClickHandler.script.run({ + this: this.layoutDoc, + self: this.rootDoc, + scriptContext: this.props.scriptContext, + thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView(), + clientX: e.clientX, + clientY: e.clientY, + shiftKey: e.shiftKey + }, console.log); + UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); + } else if (!Doc.IsSystem(this.props.Document)) { + if (this.props.Document.type !== DocumentType.LABEL) { + UndoManager.RunInBatch(() => this.props.addDocTab((this.rootDoc._fullScreenView as Doc) || this.rootDoc, "lightbox"), "double tap"); + SelectionManager.DeselectAll(); } + Doc.UnBrushDoc(this.props.Document); } } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const shiftKey = e.shiftKey; @@ -433,19 +465,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clientY: clientY, shiftKey }, console.log); - const clickFunc = () => { - if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && - !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) && - !this.onClickHandler.script.originalScript.includes("selectMainMenu")) { - UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on click"); - } else func(); - }; + const clickFunc = () => this.props.Document.dontUndo ? func() : + UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on click"); if (this.onDoubleClickHandler) { this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); } else clickFunc(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); - } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey && !(e.nativeEvent as any).formattedHandled) { this.allLinks.length && LinkManager.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 @@ -480,7 +507,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { e.stopPropagation(); - if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it + // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though + //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -519,7 +547,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); } else { this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); - this._lastTap = Date.now(); + // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them + if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected } } @@ -579,7 +608,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const linkSource = de.complete.annoDragData ? de.complete.annoDragData.annotationDocument : de.complete.linkDragData ? de.complete.linkDragData.linkSourceDocument : undefined; if (linkSource && linkSource !== this.props.Document) { e.stopPropagation(); - const dropDoc = this._componentView?.getAnchor() || this.rootDoc; + const dropDoc = this._componentView?.getAnchor?.() || this.rootDoc; if (de.complete.annoDragData) de.complete.annoDragData.dropDocument = dropDoc; de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: dropDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); } @@ -634,6 +663,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); + !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { @@ -649,6 +679,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); + onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" }); if (!this.Document.annotationOn) { const options = cm.findByDescription("Options..."); @@ -721,11 +752,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - setContentView = (view: { getAnchor: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view; + setContentView = (view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view; @observable contentsActive: () => boolean = returnFalse; @action setContentsActive = (setActive: () => boolean) => this.contentsActive = setActive; @computed get contents() { TraceMobx(); + const audioView = !this.layoutDoc._showAudio ? (null) : + <div className="documentView-audioBackground" + onPointerDown={this.recordAudioAnnotation} + onPointerEnter={this.onPointerEnter} + style={{ height: 25, position: "absolute", top: 10, left: 10 }} + > + <FontAwesomeIcon className="documentView-audioFont" + style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> + </div>; return <div className="documentView-contentsView" style={{ pointerEvents: this.props.contentPointerEvents as any, @@ -746,6 +787,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} {this.hideLinkButton ? (null) : <DocumentLinksButton View={this.props.DocumentView()} links={this.allLinks} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} + + {audioView} </div>; } @@ -785,11 +828,62 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div >); } + @action + onPointerEnter = () => { + const self = this; + const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); + if (audioAnnos && audioAnnos.length && this._audioState === 0) { + const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; + anno.data instanceof AudioField && new Howl({ + src: [anno.data.url.href], + format: ["mp3"], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => self._audioState = 0); + } + }); + this._audioState = 1; + } + } + recordAudioAnnotation = () => { + let gumStream: any; + let recorder: any; + const self = this; + navigator.mediaDevices.getUserMedia({ + audio: true + }).then(function (stream) { + gumStream = stream; + recorder = new MediaRecorder(stream); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(e.data); + if (!(result instanceof Error)) { + const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = "layout"; + const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc)); + if (audioAnnos === undefined) { + self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } + } + }; + runInAction(() => self._audioState = 2); + recorder.start(); + setTimeout(() => { + recorder.stop(); + runInAction(() => self._audioState = 0); + gumStream.getAudioTracks()[0].stop(); + }, 5000); + }); + } + captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); @computed get innards() { TraceMobx(); - const showTitle = this.ShowTitle; - const showTitleHover = StrCast(this.layoutDoc._showTitleHover); + const showTitle = this.ShowTitle?.split(":")[0]; + const showTitleHover = this.ShowTitle?.includes(":hover"); const showCaption = StrCast(this.layoutDoc._showCaption); const captionView = !showCaption ? (null) : <div className="documentView-captionWrapper" @@ -810,7 +904,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const titleView = !showTitle ? (null) : <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{ position: this.headerMargin ? "relative" : "absolute", - height: this.headerMargin, + height: this.titleHeight, background: SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"), pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, }}> @@ -927,9 +1021,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); contentsActive = () => this.docView?.contentsActive(); - focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => { - return this.docView?.focus(doc, willZoom, scale, afterFocus); - } + focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; @@ -971,7 +1063,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); - select = (ctrlPressed: boolean) => SelectionManager.SelectView(this, ctrlPressed); + select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); NativeWidth = () => this.nativeWidth; NativeHeight = () => this.nativeHeight; PanelWidth = () => this.panelWidth; diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 87e49df61..ec77775be 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -99,7 +99,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc subDocs.forEach((t) => { const facetVal = t[facetKey]; if (facetVal instanceof RichTextField) rtFields++; - valueSet.add(Field.toString(facetVal as Field)); + facetVal && valueSet.add(Field.toString(facetVal as Field)); const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 41055e2db..6d60c70bf 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -94,24 +94,6 @@ height: 100%; } -.imageBox-audioBackground { - display: inline-block; - width: 10%; - position: absolute; - top: 0px; - left: 0px; - border-radius: 25px; - background: white; - opacity: 0.3; - - svg { - width: 90% !important; - height: 70%; - position: absolute; - left: 5%; - top: 15%; - } -} .imageBox-fader { position: relative; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 286b3bf5f..728e5e02f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,4 +1,3 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import { Dictionary } from 'typescript-collections'; @@ -10,12 +9,11 @@ import { ObjectField } from '../../../fields/ObjectField'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { AudioField, ImageField } from '../../../fields/URLField'; +import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, OmitKeys, returnOne, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; -import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -30,8 +28,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); const path = require('path'); -const { Howl } = require('howler'); - export const pageSchema = createSchema({ _curPage: "number", @@ -39,16 +35,6 @@ export const pageSchema = createSchema({ googlePhotosUrl: "string", googlePhotosTags: "string" }); - -interface Window { - MediaRecorder: MediaRecorder; -} - -declare class MediaRecorder { - // whatever MediaRecorder has - constructor(e: any); -} - type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; const ImageDocument = makeInterface(pageSchema, documentSchema); @@ -64,10 +50,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); + private _curSuffix = "_m"; private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; - @observable private _audioState = 0; - @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { @@ -120,37 +105,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } } - recordAudioAnnotation = () => { - let gumStream: any; - let recorder: any; - const self = this; - navigator.mediaDevices.getUserMedia({ - audio: true - }).then(function (stream) { - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); - if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(self.dataDoc[self.fieldKey + "-audioAnnotations"], listSpec(Doc)); - if (audioAnnos === undefined) { - self.dataDoc[self.fieldKey + "-audioAnnotations"] = new List([audioDoc]); - } else { - audioAnnos.push(audioDoc); - } - } - }; - runInAction(() => self._audioState = 2); - recorder.start(); - setTimeout(() => { - recorder.stop(); - runInAction(() => self._audioState = 0); - gumStream.getAudioTracks()[0].stop(); - }, 5000); - }); - } + @undoBatch + resolution = () => this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes @undoBatch rotate = action(() => { @@ -170,6 +126,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (field) { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); + funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? "Dynamic Res" : "Full Res"}`, event: this.resolution, icon: "expand-arrows-alt" }); if (!Doc.UserDoc().noviceMode) { funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); @@ -223,7 +180,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const ext = path.extname(url.href); const scrSize = this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight); - this._curSuffix = this.props.renderDepth < 1 ? "_o" : scrSize[0] < 100 ? "_s" : scrSize[0] < 400 || !this.props.isSelected() ? "_m" : "_o"; + this._curSuffix = this.props.renderDepth < 1 || this.layoutDoc._showFullRes ? "_o" : scrSize[0] < 100 ? "_s" : scrSize[0] < 400 || !this.props.isSelected() ? "_m" : "_o"; return url.href.replace(ext, this._curSuffix + ext); } @@ -247,29 +204,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } } } - _curSuffix = "_m"; - - @action - onPointerEnter = () => { - const self = this; - const audioAnnos = DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]); - if (audioAnnos && audioAnnos.length && this._audioState === 0) { - const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; - anno.data instanceof AudioField && new Howl({ - src: [anno.data.url.href], - format: ["mp3"], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => self._audioState = 0); - } - }); - this._audioState = 1; - } - } - - audioDown = () => this.recordAudioAnnotation(); considerGooglePhotosLink = () => { const remoteUrl = this.dataDoc.googlePhotosUrl; @@ -384,16 +318,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD ref={this._imgRef} onError={this.onError} /></div>} </div> - {!this.layoutDoc._showAudio ? (null) : - <div className="imageBox-audioBackground" - onPointerDown={this.audioDown} - onPointerEnter={this.onPointerEnter} - style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} - > - <FontAwesomeIcon className="imageBox-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} - icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> - </div>} {this.considerDownloadIcon} {this.considerGooglePhotosLink()} <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> @@ -418,7 +342,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD @observable _marqueeing: number[] | undefined; @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } @action marqueeDown = (e: React.PointerEvent) => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index db5414069..d76b61502 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -124,8 +124,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; const anchorScale = !this.dataDoc[this.fieldKey + "-useLinkSmallAnchor"] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; - const timecode = this.dataDoc[anchor + "_timecode"]; - const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title) + (timecode !== undefined ? ":" + timecode : ""); + const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); const flyout = ( <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} /> diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 6155ed493..716810536 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,11 +3,11 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from "mobx-react"; import wiki from "wikijs"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc"; import { NumCast, StrCast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from '../../DocServer'; -import { Docs } from "../../documents/Documents"; +import { Docs, DocUtils } from "../../documents/Documents"; import { LinkManager } from '../../util/LinkManager'; import { Transform } from "../../util/Transform"; import { undoBatch } from '../../util/UndoManager'; @@ -45,7 +45,11 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { if (anchor1 && anchor2) { linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1; } - this._targetDoc = linkTarget?.annotationOn as Doc ?? linkTarget; + if (linkTarget?.annotationOn) { + linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => this._targetDoc = anno)); + } else { + this._targetDoc = linkTarget; + } this._toolTipText = ""; } componentDidUpdate(props: any) { @@ -99,6 +103,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { followLink = (e: React.PointerEvent) => { if (this._linkDoc && this._linkSrc) { + LinkDocPreview.Clear(); LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _width: 200, _height: 400, useCors: true }), "add:right"); @@ -161,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { renderDepth={-1} PanelWidth={this.width} PanelHeight={this.height} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} whenActiveChanged={returnFalse} bringToFront={returnFalse} NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 989be1ab9..33147e7f3 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -80,7 +80,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum } } - scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => this._pdfViewer?.scrollFocus(doc, smooth, afterFocus); + initialScrollTarget: Opt<Doc>; + scrollFocus = (doc: Doc, smooth: boolean) => { + this.initialScrollTarget = doc; + return this._pdfViewer?.scrollFocus(doc, smooth); + } getAnchor = () => this.rootDoc; componentWillUnmount() { this._selectReactionDisposer?.(); } componentDidMount() { @@ -128,7 +132,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum }); whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); - setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; + setPdfViewer = (pdfViewer: PDFViewer) => { + this._pdfViewer = pdfViewer; + if (this.initialScrollTarget) { + this.scrollFocus(this.initialScrollTarget, false); + this.initialScrollTarget = undefined; + } + } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; settingsPanel() { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 589a1c2ae..b9480fa74 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -70,6 +70,7 @@ export class PinProps { audioRange?: boolean; unpin?: boolean; setPosition?: boolean; + hidePresBox?: boolean; } type PresBoxSchema = makeInterface<[typeof documentSchema]>; @@ -422,7 +423,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.layoutDoc.presCollection = targetDoc; // this still needs some fixing setTimeout(resetSelection, 500); - doc !== targetDoc && setTimeout(() => finished?.(), 100); /// give it some time to create the targetDoc if we're opening up its context + if (doc !== targetDoc) { + setTimeout(() => finished?.(), 100); /// give it some time to create the targetDoc if we're opening up its context + } else { + finished?.(); + } }; // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { @@ -715,8 +720,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (audio) { audio.mediaStart = "manual"; audio.mediaStop = "manual"; - audio.presStartTime = NumCast(doc.audioStart, NumCast(doc.videoStart)); - audio.presEndTime = NumCast(doc.audioEnd, NumCast(doc.videoEnd)); + audio.presStartTime = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); + audio.presEndTime = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); audio.presDuration = audio.presStartTime - audio.presEndTime; TabDocView.PinDoc(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index c0247c226..435563255 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, StrListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { makeInterface } from "../../../fields/Schema"; @@ -71,7 +71,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD getAnchor = () => { const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey + "-timeline", "videoStart", "videoEnd", timecode ? timecode : undefined) || this.rootDoc; + const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; + return anchor; } choosePath(url: string) { @@ -202,7 +203,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - this._disposers.selection = reaction(() => this.props.isSelected(), selected => { if (!selected) { @@ -494,10 +494,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} renderDepth={this.props.renderDepth + 1} - startTag={"videoStart"} - endTag={"videoEnd"} - fieldKeySuffix={"-timeline"} - focus={emptyFunction} + startTag={"_timecodeToShow" /* videoStart */} + endTag={"_timecodeToHide" /* videoEnd */} bringToFront={emptyFunction} CollectionView={undefined} duration={this.duration} @@ -540,7 +538,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100; marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0]; - + timelineDocFilter = () => ["_timelineLabel:true:x"]; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; @@ -558,6 +556,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD select={emptyFunction} active={this.annotationsActive} scaling={returnOne} + docFilters={this.timelineDocFilter} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.screenToLocalTransform} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index ef0df25b6..663a8b8e4 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -30,6 +30,7 @@ import { DocAfterFocusFunc } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; import React = require("react"); +import { LinkDocPreview } from "./LinkDocPreview"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -111,21 +112,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } getAnchor = () => this.rootDoc; - scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => { + scrollFocus = (doc: Doc, smooth: boolean) => { + let focusSpeed: Opt<number>; if (doc !== this.rootDoc && this.webpage && this._outerRef.current) { const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { this._initialScroll !== undefined && (this._initialScroll = scrollTo); this._ignoreScroll = true; - this.goTo(scrollTo, smooth ? 500 : 0); - this.layoutDoc._scrollTop = scrollTo; + this.goTo(scrollTo, focusSpeed = smooth ? 500 : 0); + if (!LinkDocPreview.LinkInfo) { + this.layoutDoc._scrollTop = scrollTo; + } this._ignoreScroll = false; - return afterFocus?.(true); } } else { this._initialScroll = NumCast(doc.y); } - afterFocus?.(false); + + return focusSpeed; } async componentDidMount() { @@ -424,7 +428,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} showInfo={emptyFunction} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={emptyFunction} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 183719e31..da7ed9ac7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -155,7 +155,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBox.Instance = this; this.updateHighlights(); this._recordingStart = Date.now(); - this.layoutDoc._timeStampOnEnter = true; } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -259,13 +258,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp _lastText = ""; dispatchTransaction = (tx: Transaction) => { - let timeStamp; - clearTimeout(timeStamp); if (this._editorView) { - const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); if (metadata) { - const range = tx.selection.$from.blockRange(tx.selection.$to); let text = range ? tx.doc.textBetween(range.start, range.end) : ""; let textEndSelection = tx.selection.to; @@ -301,8 +296,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); - if (!this._editorView.state.selection.empty && FormattedTextBox.CanAnnotate) this.setupAnchorMenu(); - const removeSelection = (json: string | undefined) => { return json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); }; @@ -313,15 +306,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || json.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (removeSelection(json) !== removeSelection(curLayout?.Data)) { - if (!this._pause && !this.layoutDoc._timeStampOnEnter) { - timeStamp = setTimeout(() => this.pause(), 10 * 1000); // 10 seconds delay for time stamp - } - - // if 10 seconds have passed, insert time stamp the next time you type - if (this._pause) { - this._pause = false; - this.insertTime(); - } !curText && tx.storedMarks?.filter(m => m.type.name === "pFontSize").map(m => Doc.UserDoc().fontSize = this.layoutDoc._fontSize = (m.attrs.fontSize + "px")); !curText && tx.storedMarks?.filter(m => m.type.name === "pFontFamily").map(m => Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); @@ -355,8 +339,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } - pause = () => this._pause = true; - // for inserting timestamps insertTime = () => { let linkTime; @@ -364,11 +346,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp DocListCast(this.dataDoc.links).forEach((l, i) => { const anchor = (l.anchor1 as Doc).annotationOn ? l.anchor1 as Doc : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; if (anchor && (anchor.annotationOn as Doc).audioState === "recording") { - linkTime = NumCast(anchor.audioStart); + linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; } }); - if (this._editorView) { + if (this._editorView && linkTime) { const state = this._editorView.state; const now = Date.now(); let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); @@ -384,7 +366,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const path = (this._editorView.state.selection.$from as any).path; - if (linkAnchor && linkTime && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { + if (linkAnchor && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; const from = state.selection.from; @@ -664,7 +646,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp uicontrols.push({ description: `${FormattedTextBox.CanAnnotate ? "Hide" : "Show"} Annotation Bar`, event: () => FormattedTextBox.CanAnnotate = !FormattedTextBox.CanAnnotate, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); - !Doc.UserDoc().noviceMode && uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" @@ -868,7 +849,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return this.active();//this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; } - scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => { + focusSpeed: Opt<number>; + scrollFocus = (doc: Doc, smooth: boolean) => { const anchorId = doc[Id]; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; @@ -899,21 +881,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const ret = findAnchorFrag(editor.state.doc.content, editor); if (ret.frag.size > 2 && ret.start >= 0) { + smooth && (this.focusSpeed = 500); let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); - const escAnchorId = anchorId[0] > '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId; + const escAnchorId = anchorId[0] >= '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" }); - setTimeout(() => { - clearStyleSheetRules(FormattedTextBox._highlightStyleSheet); - afterFocus?.(true); - }, 1500); + setTimeout(() => this.focusSpeed = undefined, this.focusSpeed); + setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this.focusSpeed || 0, 1500)); } - } else { - afterFocus?.(false); } + + return this.focusSpeed; } componentDidMount() { @@ -1224,8 +1205,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const scrollRef = self._scrollRef.current; if ((docPos.top < viewRect.top || docPos.top > viewRect.bottom) && scrollRef) { const scrollPos = scrollRef.scrollTop + (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale; - if (!LinkDocPreview.LinkInfo) { - scrollPos && smoothScroll(500, scrollRef, scrollPos); + if (this.focusSpeed !== undefined) { + scrollPos && smoothScroll(this.focusSpeed, scrollRef, scrollPos); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1335,24 +1316,34 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBoxComment.textBox = this; if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar - e.stopPropagation(); // if the text box is selected, then it consumes all down events + // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView, + // but that's changed, so this shouldn't be needed. + //e.stopPropagation(); // if the text box is selected, then it consumes all down events + document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointermove", this.onSelectMove); } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } } + onSelectMove = (e: PointerEvent) => e.stopPropagation(); + onSelectEnd = (e: PointerEvent) => { + document.removeEventListener("pointerup", this.onSelectEnd); + document.removeEventListener("pointermove", this.onSelectMove); + } onPointerUp = (e: React.PointerEvent): void => { + FormattedTextBox.CanAnnotate = true; + if (!this._editorView?.state.selection.empty && FormattedTextBox.CanAnnotate) this.setupAnchorMenu(); if (!this._downEvent) return; this._downEvent = false; if (!(e.nativeEvent as any).formattedHandled && this.active(true)) { const editor = this._editorView!; - FormattedTextBoxComment.textBox = this; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); const target = (e.target as any).parentElement; // hrefs are store don the database of the <a> node that wraps the hyerlink <span> - FormattedTextBoxComment.update(editor, undefined, target?.dataset?.targethrefs); + FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs); } (e.nativeEvent as any).formattedHandled = true; @@ -1446,10 +1437,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } - (e.nativeEvent as any).formattedHandled = true; + this.props.isSelected(true) && ((e.nativeEvent as any).formattedHandled = true); if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events - e.stopPropagation(); + // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; @@ -1585,7 +1576,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); } else { if (e.key === "Tab" || e.key === "Enter") { - if (e.key === "Enter" && this.layoutDoc._timeStampOnEnter) { + if (e.key === "Enter") { this.insertTime(); } e.preventDefault(); @@ -1659,8 +1650,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + fitToBox = () => this.props.Document._fitToBox; @computed get sidebarCollection() { - const fitToBox = this.props.Document._fitToBox; const collectionProps: SubCollectionViewProps & collectionFreeformViewProps = { ...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit, NativeWidth: returnZero, @@ -1673,7 +1664,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp scaleField: this.SidebarKey + "-scale", isAnnotationOverlay: true, fieldKey: this.SidebarKey, - fitContentsToDoc: fitToBox, + fitContentsToDoc: this.fitToBox, select: emptyFunction, active: this.annotationsActive, scaling: this.sidebarContentScaling, diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 89df5e246..15c669338 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -82,9 +82,10 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.display = ""; } - static update(view: EditorView, lastState?: EditorState, hrefs: string = "") { - if (FormattedTextBoxComment.textBox && (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) { - FormattedTextBoxComment.setupPreview(view, FormattedTextBoxComment.textBox, hrefs ? hrefs.trim().split(" ") : undefined); + static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = "") { + FormattedTextBoxComment.textBox = textBox; + if ((hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) { + FormattedTextBoxComment.setupPreview(view, textBox, hrefs ? hrefs.trim().split(" ") : undefined); } } @@ -114,7 +115,7 @@ export class FormattedTextBoxComment { nbef && naft && LinkDocPreview.SetLinkInfo({ docProps: textBox.props, linkSrc: textBox.rootDoc, - location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - nbef)), + location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), hrefs, showHeader: true }); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 8d9d36580..243cfc6de 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -49,7 +49,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey /// bcz; Argh!! replace with an onEnter func that conditionally handles Enter const addTextBox = (below: boolean, force?: boolean) => { - if (props.Document.treeViewOutlineMode) return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored.. + if (props.Document.treeViewType === "outline") return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored.. const layoutDoc = props.Document; const originalDoc = layoutDoc.rootDocument || layoutDoc; if (force || props.Document._singleLine) { diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 99334b6db..e5d924f42 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -262,9 +262,10 @@ export class RichTextRules { }), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - // [[ <fieldKey> : <Doc>]] + // [[<fieldKey> : <Doc>]] // [[:Doc]] => hyperlink // [[fieldKey]] => show field + // [[fieldKey=value]] => show field and also set its value // [[fieldKey:Doc]] => show field of doc new InputRule( new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), @@ -274,15 +275,22 @@ export class RichTextRules { const docid = rawdocid ? normalizeEmail((!rawdocid.includes("@") ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1))) : undefined; const value = match[2]?.substring(1); if (!fieldKey) { - const linkId = Utils.GenerateGuid(); if (docid) { DocServer.GetRefField(docid).then(docx => { + const rstate = this.TextBox.EditorView?.state; + const selection = rstate?.selection.$from.pos; + if (rstate) { + this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); + } const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500, }, docid); - DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to", undefined, linkId); + DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined); + + const fstate = this.TextBox.EditorView?.state; + if (fstate && selection) { + this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); + } }); - const allLinks = [{ href: Utils.prepend("/doc/" + docid), title: docid, targetId: docid, linkId }]; - const link = state.schema.marks.linkAnchor.create({ allLinks, title: rawdocid, location: "add:right" }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2); } return state.tr; } |
