diff options
Diffstat (limited to 'src')
59 files changed, 1032 insertions, 623 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index de4a8f43c..d1a77f2ec 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -35,6 +35,7 @@ import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; +import { DocFocusOptions } from "../views/nodes/DocumentView"; import { FilterBox } from "../views/nodes/FilterBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; @@ -75,8 +76,7 @@ export interface DocumentOptions { _fitWidth?: boolean; _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents _freeformLOD?: boolean; // whether to use LOD to render a freeform document - _showTitleHover?: string; // - _showTitle?: string; // which field to display in the title area. leave empty to have no title + _showTitle?: string; // field name to display in header (:hover is an optional suffix) _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _scrollTop?: number; // scroll location for pdfs _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged @@ -111,6 +111,7 @@ export interface DocumentOptions { toolTip?: string; // tooltip to display on hover style?: string; page?: number; + dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel) description?: string; // added for links _viewScale?: number; _overflow?: string; @@ -144,6 +145,7 @@ export interface DocumentOptions { _layers?: List<string>; _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged. isLinkButton?: boolean; + isFolder?: boolean; _columnWidth?: number; _fontSize?: string; _fontWeight?: number; @@ -153,6 +155,7 @@ export interface DocumentOptions { _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) _timecodeToHide?: number; // the time that a document should be hidden + _timelineLabel?: boolean; // whether the document exists on a timeline lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection appearFrame?: number; // the frame in which the document appears @@ -210,7 +213,7 @@ export interface DocumentOptions { treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked treeViewTruncateTitleWidth?: number; - treeViewOutlineMode?: boolean; // whether slide should function as a text outline + treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy treeViewLockExpandedView?: boolean; // whether the expanded view can be changed treeViewDefaultExpandedView?: string; // default expanded view sidebarColor?: string; // background color of text sidebar @@ -1017,6 +1020,10 @@ export namespace DocUtils { }); } + export function DefaultFocus(doc: Doc, options?: DocFocusOptions) { + options?.afterFocus?.(false); + } + export let ActiveRecordings: AudioBox[] = []; export function MakeLinkToActiveAudio(doc: Doc) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e7a3ae10d..8cb5e3903 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -210,7 +210,7 @@ export class CurrentUserUtils { doc["template-button-detail"] = CurrentUserUtils.ficon({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(detailView) as any as Doc, title: "detail view", icon: "window-maximize", system: true + dragFactory: new PrefetchProxy(detailView) as any as Doc, title: "detailView", icon: "window-maximize", system: true }); } @@ -378,7 +378,7 @@ export class CurrentUserUtils { ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptySlide === undefined) { - const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewOutlineMode: true, _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, _backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); + const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, _backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); FormattedTextBox.SelectOnLoad = textDoc[Id]; doc.emptySlide = textDoc; @@ -548,6 +548,7 @@ export class CurrentUserUtils { iconShape: "square", _stayInCollection: true, _hideContextMenu: true, + dontUndo: true, title, target, _backgroundColor: "black", @@ -578,6 +579,7 @@ export class CurrentUserUtils { DocListCastAsync(btns).then(bts => bts?.forEach(btn => { btn.color = "white"; btn._backgroundColor = ""; + btn.dontUndo = true; })); }); }); @@ -851,14 +853,16 @@ export class CurrentUserUtils { /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); + doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); } if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); + doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); } if (doc.dockedBtns === undefined) { doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); } + (doc["dockedBtn-undo"] as Doc).dontUndo = true; + (doc["dockedBtn-redo"] as Doc).dontUndo = true; } // sets up the default set of documents to be shown in the Overlay layer static setupOverlays(doc: Doc) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 816f7f6be..67e05f8d0 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -7,10 +7,9 @@ import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; import { LightboxView } from '../views/LightboxView'; -import { DocumentView } from '../views/nodes/DocumentView'; +import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; import { Scripting } from './Scripting'; - -export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; +import { CurrentUserUtils } from './CurrentUserUtils'; export class DocumentManager { @@ -104,7 +103,7 @@ export class DocumentManager { public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const docViews = DocumentManager.Instance.DocumentViews; const views: DocumentView[] = []; - docViews.map(view => LightboxView.IsLightboxDocView(view.docViewPath) && view.rootDoc === toFind && views.push(view)); + docViews.map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); } public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { @@ -141,13 +140,11 @@ export class DocumentManager { closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc finished?: () => void, + originalTarget?: Doc ): Promise<void> => { + originalTarget = originalTarget ?? targetDoc; const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; const docView = getFirstDocView(targetDoc, originatingDoc); - const highlight = () => { - const finalDocView = getFirstDocView(targetDoc); - finalDocView && Doc.linkFollowHighlight(finalDocView.rootDoc); - }; const focusAndFinish = (didFocus: boolean) => { if (originatingDoc?.isPushpin) { if (!didFocus || targetDoc.hidden) { @@ -157,36 +154,32 @@ export class DocumentManager { targetDoc.hidden && (targetDoc.hidden = undefined); docView?.select(false); } - highlight(); finished?.(); return false; }; - let annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); + const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); + const rtfView = annotatedDoc && getFirstDocView(annotatedDoc); const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined; const targetDocContext = contextDoc || annotatedDoc; const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext); - if (!docView && annotatedDoc && annotatedDoc !== originatingDoc?.context && targetDoc.type === DocumentType.TEXTANCHOR) { - const first = getFirstDocView(annotatedDoc); - if (first) { - annotatedDoc = first.rootDoc; - first.focus(targetDoc, false); - } - } else if (docView && (targetDocContextView || !targetDocContext)) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? - docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => - new Promise<boolean>(res => { - focusAndFinish(didFocus); - res(); - }) - ); + const focusView = !docView && targetDoc.type === DocumentType.TEXTANCHOR && rtfView ? rtfView : docView; + if (focusView) { + focusView && Doc.linkFollowHighlight(focusView.rootDoc); + focusView.focus(targetDoc, { + originalTarget, willZoom, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + focusAndFinish(didFocus); + res(); + }) + }); } else { if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); - highlight(); } else { // otherwise try to get a view of the context of the target if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. targetDocContext._viewTransition = "transform 500ms"; - targetDocContextView.props.focus(targetDocContextView.rootDoc, willZoom); + targetDocContextView.props.focus(targetDocContextView.rootDoc, { willZoom }); // now find the target document within the context if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; @@ -196,18 +189,18 @@ export class DocumentManager { const findView = (delay: number) => { const retryDocView = getFirstDocView(targetDoc); // test again for the target view snce we presumably created the context above by focusing on it if (retryDocView) { // we found the target in the context - retryDocView.props.focus(targetDoc, willZoom, undefined, (didFocus: boolean) => - new Promise<boolean>(res => { - focusAndFinish(didFocus); - res(); - }) - ); // focus on the target in the context - highlight(); + retryDocView.props.focus(targetDoc, { + willZoom, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + focusAndFinish(didFocus); + res(); + }) + }); // focus on the target in the context } else if (delay > 1500) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc); if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document - Doc.SetInPlace(targetDoc, "annotationOn", undefined, false); + // Doc.SetInPlace(targetDoc, "annotationOn", undefined, false); createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } } else { @@ -217,18 +210,13 @@ export class DocumentManager { findView(0); } } else { // there's no context view so we need to create one first and try again when that finishes - const finishFunc = () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished); - if (LightboxView.LightboxDoc) { - runInAction(() => LightboxView.LightboxDoc = targetDocContext); - setTimeout(() => finishFunc, 250); - } else { - createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target - finishFunc); - } + const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget); + createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target + finishFunc); } } } } } -Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file +Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, { willZoom: true })); });
\ No newline at end of file diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 58ccfe645..402cbdd68 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -3,11 +3,12 @@ import { computedFn } from "mobx-utils"; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { BoolCast, Cast, StrCast } from "../../fields/Types"; import { LightboxView } from "../views/LightboxView"; -import { DocumentViewSharedProps } from "../views/nodes/DocumentView"; -import { CreateViewFunc, DocumentManager } from "./DocumentManager"; +import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; +import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; +type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* * link doc: * - anchor1: doc @@ -104,19 +105,22 @@ export class LinkManager { const batch = UndoManager.StartBatch("follow link click"); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { - const createTabForTarget = (didFocus: boolean) => new Promise<boolean>(res => { - const where = StrCast(sourceDoc.followLinkLocation) || followLoc; + const createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => { + const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation) || followLoc; docViewProps.addDocTab(doc, where); setTimeout(() => { const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); if (targDocView) { - targDocView.props.focus(doc, BoolCast(sourceDoc.followLinkZoom, false), undefined, (didFocus: boolean) => { - finished?.(); - res(true); - return new Promise<boolean>(res2 => res2()); + targDocView.props.focus(doc, { + willZoom: BoolCast(sourceDoc.followLinkZoom, false), + afterFocus: (didFocus: boolean) => { + finished?.(); + res(ViewAdjustment.resetView); + return new Promise<ViewAdjustment>(res2 => res2()); + } }); } else { - res(where !== "inPlace"); // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) + res(where !== "inPlace" ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target } }); }); @@ -125,38 +129,30 @@ export class LinkManager { createTabForTarget(false); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - docViewProps.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, createTabForTarget); + docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); } }; LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } - public static traverseLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { - const linkDocs = link ? [link] : DocListCast(doc.links); - const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 - const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 + + public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { + const linkDocs = link ? [link] : DocListCast(sourceDoc.links); + const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1 + const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2 const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs); - const followLinks = linkDocList.length ? (doc.isPushpin ? linkDocList : [linkDocList[0]]) : []; + const followLinks = linkDocList.length ? (sourceDoc.isPushpin ? linkDocList : [linkDocList[0]]) : []; followLinks.forEach(async linkDoc => { if (linkDoc) { - const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; - const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : - doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); + const target = (sourceDoc === linkDoc.anchor1 ? linkDoc.anchor2 : sourceDoc === linkDoc.anchor2 ? linkDoc.anchor1 : + (Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; if (target) { - if (LightboxView.LightboxDoc && !DocumentManager.Instance.getLightboxDocumentView(doc)) { - runInAction(() => LightboxView.LightboxDoc = (target.annotationOn as Doc) ?? target); - finished?.(); - } else { - const containerDoc = Cast(target.annotationOn, Doc, null) || target; - targetTimecode !== undefined && (containerDoc._currentTimecode = targetTimecode); - const targetContext = Cast(containerDoc?.context, Doc, null); - const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, doc, finished); - } + const containerDoc = Cast(target.annotationOn, Doc, null) || target; + const targetContext = Cast(containerDoc?.context, Doc, null); + const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); } else { finished?.(); } diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index ab6c94f83..3b23d5d56 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -158,6 +158,10 @@ declare abstract class URLField extends ObjectField { constructor(url: URL); } +declare class RichTextField extends URLField { + [Copy](): ObjectField; + constructor(data:string, text: string); +} declare class AudioField extends URLField { [Copy](): ObjectField; } declare class VideoField extends URLField { [Copy](): ObjectField; } declare class ImageField extends URLField { [Copy](): ObjectField; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index eeef94d74..209e750d6 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -312,22 +312,20 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV return false; } - _ref = React.createRef<HTMLDivElement>(); + _ref = React.createRef<TemplateMenu>(); @observable _tooltipOpen: boolean = false; @computed get templateButton() { const view0 = this.view0; - const templates: Map<string, boolean> = new Map(); const views = this.props.views(); - Array.from(["Caption", "Title", "TitleHover"]).map(template => - templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template] ? true : false, false as boolean))); return !view0 ? (null) : <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embeddable alias</div>} open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} placement="bottom"> <div className="documentButtonBar-linkFlyout" ref={this._dragRef} - onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} > + onPointerEnter={action(() => !(this._ref.current as any as HTMLElement)?.getBoundingClientRect().width && (this._tooltipOpen = true))} > <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={!this._aliasDown ? (null) : <div ref={this._ref}> <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} /></div>}> + content={!this._aliasDown ? (null) : + <TemplateMenu ref={this._ref} docViews={views.filter(v => v).map(v => v as DocumentView)} />}> <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 77b43db9b..c8a5b338a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -175,10 +175,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b } else if (e.altKey) { // open same document in new tab CollectionDockingView.ToggleSplit(Cast(selectedDocs[0].props.Document._fullScreenView, Doc, null) || selectedDocs[0].props.Document, "right"); } else { - runInAction(() => { - LightboxView.LightboxDoc = selectedDocs[0].props.Document; - LightboxView.LightboxFuture = selectedDocs.slice(1).map(view => view.props.Document); - }); + LightboxView.SetLightboxDoc(selectedDocs[0].props.Document, undefined, selectedDocs.slice(1).map(view => view.props.Document)); } } } diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss index c9d78890e..16a4c74d1 100644 --- a/src/client/views/GestureOverlay.scss +++ b/src/client/views/GestureOverlay.scss @@ -5,6 +5,7 @@ top: 0; left: 0; touch-action: none; + z-index: -1; .pointerBubbles { width: 100%; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index d7e7e055f..b4051194f 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -873,7 +873,7 @@ export class GestureOverlay extends Touchable { styleProvider={returnEmptyString} layerProvider={undefined} docViewPath={returnEmptyDoclist} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 3c855fb0e..d4b8e9406 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -134,7 +134,7 @@ export class KeyManager { } if (doDeselect) { SelectionManager.DeselectAll(); - LightboxView.LightboxDoc = undefined; + LightboxView.SetLightboxDoc(undefined); } DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss index 35f11b699..4ea2dc2d6 100644 --- a/src/client/views/LightboxView.scss +++ b/src/client/views/LightboxView.scss @@ -17,7 +17,7 @@ border-radius: 8; color:white; opacity: 0.7; - width: 30; + width: 35; &:hover { opacity: 1; } diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 4e9491ec6..af07ead97 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -1,11 +1,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, computed } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import { Doc, Opt } from '../../fields/Doc'; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils'; +import { DocUtils } from '../documents/Documents'; +import { DocumentManager } from '../util/DocumentManager'; +import { LinkManager } from '../util/LinkManager'; +import { SelectionManager } from '../util/SelectionManager'; import { Transform } from '../util/Transform'; +import { TabDocView } from './collections/TabDocView'; import "./LightboxView.scss"; import { DocumentView } from './nodes/DocumentView'; import { DefaultStyleProvider } from './StyleProvider'; @@ -18,88 +24,238 @@ interface LightboxViewProps { @observer export class LightboxView extends React.Component<LightboxViewProps> { - @observable public static LightboxDoc: Opt<Doc>; - public static IsLightboxDocView(path: DocumentView[]) { return path.includes(LightboxView.LightboxDocView.current!); } - public static LightboxHistory: (Opt<Doc>)[] = []; - public static LightboxFuture: (Opt<Doc>)[] = []; - public static LightboxDocView = React.createRef<DocumentView>(); + @computed public static get LightboxDoc() { return this._doc; } + @observable private static _doc: Opt<Doc>; + @observable private static _docTarget: Opt<Doc>; + @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target + @observable private static _docFilters: string[] = []; // filters + private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, transition: Opt<string> }>; + private static _history: Opt<{ doc: Doc, target?: Doc }[]> = []; + private static _future: Opt<Doc[]> = []; + private static _docView: Opt<DocumentView>; + static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, transition: Opt<string> }> }[] = []; + @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[]) { + if (!doc) { + this._docFilters && (this._docFilters.length = 0); + if (this.LightboxDoc) { + this.LightboxDoc._panX = this._savedState?.panX; + this.LightboxDoc._panY = this._savedState?.panY; + this.LightboxDoc._viewScale = this._savedState?.scale; + this.LightboxDoc._viewTransition = this._savedState?.transition; + } + this._future = this._history = []; + } else { + this._history ? this._history.push({ doc, target }) : this._history = [{ doc, target }]; + this._savedState = { + panX: Cast(doc._panX, "number", null), + panY: Cast(doc._panY, "number", null), + scale: Cast(doc._viewScale, "number", null), + transition: Cast(doc._viewTransition, "string", null) + }; + } + if (future) { + this._future = future.slice().sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)).sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length); + } + this._doc = doc; + this._docTarget = target || doc; + this._tourMap = DocListCast(doc?.links).map(link => { + const opp = LinkManager.getOppositeAnchor(link, doc!); + return opp?.TourMap ? opp : undefined; + }).filter(m => m).map(m => m!); + + return true; + } + public static IsLightboxDocView(path: DocumentView[]) { return path.includes(this._docView!); } @computed get leftBorder() { return Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]); } @computed get topBorder() { return Math.min(this.props.PanelHeight / 4, this.props.maxBorder[1]); } lightboxWidth = () => this.props.PanelWidth - this.leftBorder * 2; lightboxHeight = () => this.props.PanelHeight - this.topBorder * 2; lightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1); - navBtn = (left: Opt<number>, icon: string, display: () => string, click: (e: React.MouseEvent) => void) => { + navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => { return <div className="lightboxView-navBtn-frame" style={{ display: display(), left, - width: Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]) + width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), + bottom }}> - <div className="lightboxView-navBtn" style={{ top: this.props.PanelHeight / 2 - 12.50 }} + <div className="lightboxView-navBtn" title={color} style={{ top, color: color ? "red" : "white", background: color ? "white" : undefined }} onClick={click}> + <div style={{ height: 10 }}>{color}</div> <FontAwesomeIcon icon={icon as any} size="3x" /> </div> </div>; } + public static GetSavedState(doc: Doc) { + return this.LightboxDoc === doc && this._savedState ? this._savedState : undefined; + } + public static SetDocFilter(filter: string) { + if (this.LightboxDoc && filter) { + this._docFilters = [`cookies:${filter}:match`]; + } + } + public static AddDocTab = (doc: Doc, location: string) => { + SelectionManager.DeselectAll(); + return LightboxView.SetLightboxDoc(doc, undefined, + [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), + ...DocListCast(doc[Doc.LayoutFieldKey(doc) + "-annotations"]), + ...(LightboxView._future ?? []) + ] + .sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))); + } + docFilters = () => LightboxView._docFilters || []; + addDocTab = LightboxView.AddDocTab; + @action + stepForward = () => { + const doc = LightboxView._doc!; + const target = LightboxView._docTarget = LightboxView._future?.pop(); + const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (docView && target) { + docView.focus(target, { willZoom: true, scale: 0.9 }); + if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target: LightboxView._docTarget }); + } else { + if (!target && LightboxView.path.length) { + const saved = LightboxView._savedState; + if (LightboxView.LightboxDoc) { + LightboxView.LightboxDoc._panX = saved?.panX; + LightboxView.LightboxDoc._panY = saved?.panY; + LightboxView.LightboxDoc._viewScale = saved?.scale; + LightboxView.LightboxDoc._viewTransition = saved?.transition; + } + const pop = LightboxView.path.pop(); + if (pop) { + LightboxView._doc = pop.doc; + LightboxView._docTarget = pop.target; + LightboxView._future = pop.future; + LightboxView._history = pop.history; + LightboxView._savedState = pop.saved; + } + } else { + LightboxView.SetLightboxDoc(target); + } + } + LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links).map(link => { + const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!); + return opp?.TourMap ? opp : undefined; + }).filter(m => m).map(m => m!); + } + @action + stepBackward = () => { + const previous = LightboxView._history?.pop(); + if (!previous || !LightboxView._history?.length) { + LightboxView.SetLightboxDoc(undefined); + return; + } + const { doc, target } = LightboxView._history?.lastElement()!; + const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (docView && target) { + LightboxView._doc = doc; + LightboxView._docTarget = target || doc; + if (LightboxView._future?.lastElement() !== previous.target || previous.doc) LightboxView._future?.push(previous.target || previous.doc); + docView.focus(target, { willZoom: true, scale: 0.9 }); + } else { + LightboxView._doc = doc; + LightboxView._docTarget = target || doc; + } + LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links).map(link => { + const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!); + return opp?.TourMap ? opp : undefined; + }).filter(m => m).map(m => m!); + } + @action + stepInto = () => { + LightboxView.path.push({ + doc: LightboxView.LightboxDoc, + target: LightboxView._docTarget, + future: LightboxView._future, + history: LightboxView._history, + saved: LightboxView._savedState + }); + const tours = LightboxView._tourMap; + if (tours && tours.length) { + const fieldKey = Doc.LayoutFieldKey(tours[0]); + LightboxView._future?.push(...DocListCast(tours[0][fieldKey]).reverse()); + } else { + const coll = LightboxView._docTarget; + if (coll) { + const fieldKey = Doc.LayoutFieldKey(coll); + LightboxView.SetLightboxDoc(coll, undefined, [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + "-annotations"])]); + TabDocView.PinDoc(coll, { hidePresBox: true }); + } + } + setTimeout(() => this.stepForward()); + } + fitToBox = () => LightboxView._docTarget === LightboxView.LightboxDoc; render() { - if (LightboxView.LightboxHistory.lastElement() !== LightboxView.LightboxDoc) LightboxView.LightboxHistory.push(LightboxView.LightboxDoc); let downx = 0, downy = 0; return !LightboxView.LightboxDoc ? (null) : <div className="lightboxView-frame" onPointerDown={e => { downx = e.clientX; downy = e.clientY; }} - onClick={action(e => { + onClick={e => { if (Math.abs(downx - e.clientX) < 4 && Math.abs(downy - e.clientY) < 4) { - LightboxView.LightboxHistory = []; - LightboxView.LightboxFuture = []; - LightboxView.LightboxDoc = undefined; + LightboxView.SetLightboxDoc(undefined); } - })} > + }} > <div className="lightboxView-contents" style={{ left: this.leftBorder, top: this.topBorder, width: this.lightboxWidth(), height: this.lightboxHeight() }}> - <DocumentView ref={LightboxView.LightboxDocView} + <DocumentView ref={action((r: DocumentView | null) => { + LightboxView._docView = r !== null ? r : undefined; + setTimeout(action(() => { + const vals = r?.ComponentView?.freeformData?.(); + if (vals && r) { + r.layoutDoc._panX = vals.panX; + r.layoutDoc._panY = vals.panY; + r.layoutDoc._viewScale = vals.scale; + } + r && (LightboxView._docTarget = undefined); + })); + })} Document={LightboxView.LightboxDoc} DataDoc={undefined} addDocument={undefined} - addDocTab={returnFalse} - pinToPres={emptyFunction} + fitContentsToDoc={this.fitToBox} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} rootSelected={returnTrue} docViewPath={returnEmptyDoclist} + docFilters={this.docFilters} removeDocument={undefined} styleProvider={DefaultStyleProvider} layerProvider={returnTrue} ScreenToLocalTransform={this.lightboxScreenToLocal} PanelWidth={this.lightboxWidth} PanelHeight={this.lightboxHeight} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} renderDepth={0} /> </div> - {this.navBtn(undefined, "chevron-left", - () => LightboxView.LightboxDoc && LightboxView.LightboxHistory.length ? "" : "none", - action(e => { + {this.navBtn(0, undefined, this.props.PanelHeight / 2 - 12.50, "chevron-left", + () => LightboxView.LightboxDoc && LightboxView._history?.length ? "" : "none", e => { e.stopPropagation(); - const popped = LightboxView.LightboxHistory.pop(); - if (LightboxView.LightboxHistory.lastElement() !== LightboxView.LightboxFuture.lastElement()) LightboxView.LightboxFuture.push(popped); - LightboxView.LightboxDoc = LightboxView.LightboxHistory.lastElement(); - }))} - {this.navBtn(this.props.PanelWidth - Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), "chevron-right", - () => LightboxView.LightboxDoc && LightboxView.LightboxFuture.length ? "" : "none", - action(e => { + this.stepBackward(); + })} + {this.navBtn(this.props.PanelWidth - Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), undefined, this.props.PanelHeight / 2 - 12.50, "chevron-right", + () => LightboxView.LightboxDoc && LightboxView._future?.length ? "" : "none", e => { e.stopPropagation(); - LightboxView.LightboxDoc = LightboxView.LightboxFuture.pop(); - }))} - + this.stepForward(); + })} + {this.navBtn("50%", 0, 0, "chevron-down", + () => LightboxView.LightboxDoc && LightboxView._future?.length ? "" : "none", e => { + e.stopPropagation(); + this.stepInto(); + }, + StrCast(LightboxView._tourMap?.lastElement()?.TourMap) + )} </div>; } }
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 729f2aa89..6010eb518 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -16,7 +16,7 @@ import { TraceMobx } from '../../fields/util'; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; -import { Docs } from '../documents/Documents'; +import { Docs, DocUtils } from '../documents/Documents'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; @@ -48,7 +48,7 @@ import "./MainView.scss"; import "./collections/TreeView.scss"; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; +import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; @@ -259,7 +259,7 @@ export class MainView extends React.Component { ScreenToLocalTransform={Transform.Identity} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -352,7 +352,7 @@ export class MainView extends React.Component { PanelHeight={this.getContentsHeight} renderDepth={0} scriptContext={CollectionDockingView.Instance.props.Document} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -382,7 +382,7 @@ export class MainView extends React.Component { PanelHeight={this.getContentsHeight} renderDepth={0} docViewPath={returnEmptyDoclist} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} styleProvider={DefaultStyleProvider} layerProvider={undefined} parentActive={returnTrue} @@ -503,7 +503,7 @@ export class MainView extends React.Component { PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} renderDepth={0} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} whenActiveChanged={emptyFunction} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -565,7 +565,7 @@ export class MainView extends React.Component { PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} renderDepth={0} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} docViewPath={returnEmptyDoclist} parentActive={returnFalse} whenActiveChanged={emptyFunction} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 64f907f4c..5696b16e9 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -15,6 +15,7 @@ import { DocumentView } from "./nodes/DocumentView"; import './OverlayView.scss'; import { ScriptingRepl } from './ScriptingRepl'; import { DefaultStyleProvider } from "./StyleProvider"; +import { DocUtils } from "../documents/Documents"; export type OverlayDisposer = () => void; @@ -191,7 +192,7 @@ export class OverlayView extends React.Component { renderDepth={1} parentActive={returnTrue} whenActiveChanged={emptyFunction} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} styleProvider={DefaultStyleProvider} layerProvider={undefined} docViewPath={returnEmptyDoclist} diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index d63eb874c..ba212da7f 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -47,6 +47,10 @@ $linkGap : 3px; background-color: white; color: black; } +.propertiesButtons-linkButton-empty.toggle-hover { + background-color: gray; + color: black; +} .propertiesButtons-linkButton-empty.toggle-off { color: white; } diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 9a836313c..53a017592 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { ImageField } from '../../fields/URLField'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; @@ -222,10 +222,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { } @undoBatch - @action - setDictation = () => { - SelectionManager.Views().forEach(dv => dv.rootDoc._showAudio = dv.rootDoc._showAudio === !dv.rootDoc._showAudio); - } + setDictation = () => SelectionManager.Views().forEach(dv => dv.rootDoc._showAudio = !dv.rootDoc._showAudio); @computed get dictationButton() { @@ -244,7 +241,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @undoBatch @action setTitle = () => { - SelectionManager.Views().forEach(dv => dv.rootDoc._showTitle = dv.rootDoc._showTitle === undefined ? "title" : undefined); + SelectionManager.Views().forEach(dv => dv.rootDoc._showTitle = !dv.rootDoc._showTitle ? "title" : dv.rootDoc._showTitle === "title" ? "title:hover" : undefined); } @computed @@ -252,7 +249,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Title Header"}</div>} placement="top"> <div> - <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showTitle ? "on" : "off"}`} onPointerDown={this.setTitle}> + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showTitle === "title" ? "on" : StrCast(targetDoc._showTitle).includes(":hover") ? "hover" : "off"}`} onPointerDown={this.setTitle}> <FontAwesomeIcon className="propertiesButtons-icon" icon="text-width" size="lg" /> </div> <div className="propertiesButtons-title"> Title </div> @@ -502,7 +499,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button"> {this.lockButton} </div> - <div className="propertiesButtons-button" style={{ display: isText || isImage ? "" : "none" }}> + <div className="propertiesButtons-button"> {this.dictationButton} </div> <div className="propertiesButtons-button"> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index e94cc71ba..e272fbaa6 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -39,7 +39,8 @@ export enum StyleProp { PointerEvents = "pointerEvents", // pointer events for DocumentView -- inherits pointer events if not specified Decorations = "decorations", // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background HeaderMargin = "headerMargin", // margin at top of documentview, typically for displaying a title -- doc contents will start below that - ShowTitle = "showTitle", // whether to display a title on a Document + TitleHeight = "titleHeight", // Height of Title area + ShowTitle = "showTitle", // whether to display a title on a Document (optional :hover suffix) } function darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } @@ -81,8 +82,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case StyleProp.HideLinkButton: return isAnchor || props?.dontRegisterView || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.ShowTitle: return doc && !doc.presentationTargetDoc && StrCast(doc._showTitle, !Doc.IsSystem(doc) && doc.type === DocumentType.RTF ? - (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : - undefined); + (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : "") || ""; case StyleProp.Color: if (isCaption) return "white"; const backColor = backgroundCol() || "black"; @@ -92,7 +92,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | return "white"; case StyleProp.Hidden: return BoolCast(doc?._hidden, BoolCast(doc?.hidden)); case StyleProp.BorderRounding: return StrCast(doc?._borderRounding, StrCast(doc?.borderRounding)); - case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && doc?._showTitle && !doc?._showTitleHover ? 15 : 0; + case StyleProp.TitleHeight: return 15; + case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && doc?._showTitle && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { if (isAnchor && docProps) return "transparent"; if (isCaption) return "rgba(0,0,0 ,0.4)"; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index cdaf1bd0c..d2e0cb265 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -45,7 +45,7 @@ class OtherToggle extends React.Component<{ checked: boolean, name: string, togg export interface TemplateMenuProps { docViews: DocumentView[]; - templates: Map<string, boolean>; + templates?: Map<string, boolean>; } @@ -115,7 +115,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const addedTypes = Doc.UserDoc().noviceMode ? [] : DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array<JSX.Element> = []; - this.props.templates.forEach((checked, template) => + this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index eee939c07..550a4082b 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -21,9 +21,9 @@ import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; import { TabDocView } from './TabDocView'; import React = require("react"); -import { stat } from 'fs'; import { DocumentType } from '../../documents/DocumentTypes'; import { listSpec } from '../../../fields/Schema'; +import { LightboxView } from '../LightboxView'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -375,13 +375,13 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const sublists = DocListCast(this.props.Document[this.props.fieldKey]); const tabs = Cast(sublists[0], Doc, null); const other = Cast(sublists[1], Doc, null); - const tabdocs = await DocListCastAsync(tabs.data); - const otherdocs = await DocListCastAsync(other.data); - Doc.GetProto(tabs).data = new List<Doc>(docs); + const tabdocs = await DocListCastAsync(tabs?.data); + const otherdocs = await DocListCastAsync(other?.data); + tabs && (Doc.GetProto(tabs).data = new List<Doc>(docs)); const otherSet = new Set<Doc>(); otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); tabdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); - Doc.GetProto(other).data = new List<Doc>(Array.from(otherSet.values())); + other && (Doc.GetProto(other).data = new List<Doc>(Array.from(otherSet.values()))); }, 0); } @@ -435,6 +435,8 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddSplit(doc, "right"); }, - "opens up the inputted document on the right side of the screen", "(doc: any)"); +Scripting.addGlobal(function openInLightbox(doc: any) { LightboxView.AddDocTab(doc, "lightbox"); }, + "opens up document in a lightbox", "(doc: any)"); +Scripting.addGlobal(function openOnRight(doc: any) { return CollectionDockingView.AddSplit(doc, "right"); }, + "opens up document in tab on right side of the screen", "(doc: any)"); Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index d6e4b01c4..423c94005 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -489,13 +489,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get lightboxButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Lightbox of Documents"}</div>} placement="top"> - <button className="antimodeMenu-button" onPointerDown={action(() => { + <button className="antimodeMenu-button" onPointerDown={() => { const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); if (docs.length) { - LightboxView.LightboxDoc = docs[0]; - LightboxView.LightboxFuture = docs.slice(1); + LightboxView.SetLightboxDoc(targetDoc, undefined, docs); } - })}> + }}> <FontAwesomeIcon className="documentdecorations-icon" icon="desktop" size="lg" /> </button> </Tooltip>; diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 904b4c761..2e6186680 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -216,7 +216,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; return ( <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} - ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> <div className={className} ref={this._focusRef} tabIndex={-1}> <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> {!this.props.Document._searchDoc ? diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 66064e354..504cf32a0 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -11,7 +11,7 @@ import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, emptyPath, returnFalse, setupMoveUpEvents, returnEmptyDoclist } from "../../../Utils"; +import { emptyFunction, emptyPath, returnFalse, setupMoveUpEvents, returnEmptyDoclist, returnTrue } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; @@ -25,6 +25,7 @@ import { DefaultStyleProvider } from "../StyleProvider"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { SchemaTable } from "./SchemaTable"; +import { DocUtils } from "../../documents/Documents"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -102,7 +103,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; @undoBatch - setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { + setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => { this._openTypes = false; if (columnTypes.get(columnField.heading)) return; @@ -113,7 +114,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { columns[index] = columnField; this.columns = columns; } - } + }); @undoBatch setColumnColor = (columnField: SchemaHeaderField, color: string): void => { @@ -401,10 +402,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <DocumentView Document={this.previewDocument} DataDoc={undefined} - fitContentsToDoc={true} + fitContentsToDoc={returnTrue} freezeDimensions={true} dontCenter={"y"} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} renderDepth={this.props.renderDepth} rootSelected={this.rootSelected} PanelWidth={this.previewWidth} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index d4eb66fcc..16a1c02f7 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -14,9 +14,10 @@ import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionSubView } from "../collections/CollectionSubView"; -import { DocumentView } from "../nodes/DocumentView"; +import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; +import { Transform } from "../../util/Transform"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -31,7 +32,6 @@ export type CollectionStackedTimelineProps = { isChildActive: () => boolean; startTag: string; endTag: string; - fieldKeySuffix?: string; }; @observer @@ -47,7 +47,6 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @observable _markerEnd: number = 0; get duration() { return this.props.duration; } - @computed get anchorDocs() { return this.props.fieldKeySuffix ? this.childDocs.concat(...DocListCast(this.rootDoc[this.props.fieldKey + this.props.fieldKeySuffix])) : this.childDocs; } @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } @computed get selectionContainer() { return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{ @@ -159,7 +158,8 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any, useLinkSmallAnchor: true, hideLinkButton: true, - annotationOn: rootDoc + annotationOn: rootDoc, + _timelineLabel: true }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -257,6 +257,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { const anchor = observable({ view: undefined as any }); + const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { + this.props.playLink(mark); + this.props.focus(doc, { willZoom, scale, afterFocus, docTransform }); + }; return { anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => anchor.view = r)} @@ -268,7 +272,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument PanelWidth={() => width} PanelHeight={() => height} ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - focus={() => this.props.playLink(mark)} + focus={focusFunc} parentActive={out => this.props.isSelected(out) || this.props.isChildActive()} rootSelected={returnFalse} onClick={script} @@ -295,7 +299,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument const timelineContentWidth = this.props.PanelWidth(); const timelineContentHeight = this.props.PanelHeight(); const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; - const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); + const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; const isActive = this.props.isChildActive() || this.props.isSelected(false); return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9c12d5020..77046f5ea 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -21,7 +21,7 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DocumentView, DocAfterFocusFunc, DocumentViewProps } from "../nodes/DocumentView"; +import { DocumentView, DocumentViewProps, DocFocusOptions, ViewAdjustment } from "../nodes/DocumentView"; import { FieldViewProps } from "../nodes/FieldView"; import { StyleProp } from "../StyleProvider"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; @@ -44,7 +44,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, _draggerRef = React.createRef<HTMLDivElement>(); _pivotFieldDisposer?: IReactionDisposer; _autoHeightDisposer?: IReactionDisposer; - _docXfs: any[] = []; + _docXfs: { height: () => number, width: () => number, stackedDocTransform: () => Transform }[] = []; _columnStart: number = 0; @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @@ -168,18 +168,23 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, return this.props.addDocTab(doc, where); } - focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => { + focusDocument = (doc: Doc, options?: DocFocusOptions) => { Doc.BrushDoc(doc); - this.props.focus(this.props.Document, true); // bcz: want our containing collection to zoom - Doc.linkFollowHighlight(doc); + let focusSpeed = 0; const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName("documentView-node")).find((node: any) => node.id === doc[Id]); if (found) { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); - smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + if (Math.floor(localTop[1]) !== 0) { + smoothScroll(focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + } } - afterFocus && setTimeout(afterFocus, 500); + const endFocus = async (moved: boolean) => options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing; + this.props.focus(this.rootDoc, { + willZoom: options?.willZoom, scale: options?.scale, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)) + }); } styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => { @@ -199,6 +204,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, let dref: Opt<HTMLDivElement>; const stackedDocTransform = () => this.getDocTransform(doc, dref); + this._docXfs.push({ stackedDocTransform, width, height }); return <DocumentView ref={r => dref = r?.ContentDiv ? r.ContentDiv : undefined} Document={doc} DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} @@ -290,8 +296,8 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, let dropAfter = 0; if (de.complete.docDragData) { this._docXfs.map((cd, i) => { - const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + const pos = cd.stackedDocTransform().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && (i === this._docXfs.length - 1 || where[1] < pos1[1])) { dropInd = i; const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1; @@ -322,8 +328,8 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, const where = [e.clientX, e.clientY]; let targInd = -1; this._docXfs.map((cd, i) => { - const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + const pos = cd.stackedDocTransform().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 287bc56c2..3cbda37e2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -112,7 +112,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const docFilters = this.docFilters(); const docRangeFilters = this.docRangeFilters(); const searchDocs = this.searchFilterDocs(); - if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) return childDocs; + if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) { + return childDocs; + } const docsforFilter: Doc[] = []; childDocs.forEach((d) => { diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss index fa7c87f4e..d995cbcd2 100644 --- a/src/client/views/collections/CollectionTimeView.scss +++ b/src/client/views/collections/CollectionTimeView.scss @@ -75,7 +75,7 @@ .collectionTimeView-innards { display: inline-block; - width: calc(100% - 200px); + width: 100%; height: 100%; } } diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index cc625e12e..8067e1d07 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,16 +1,21 @@ +import { toUpper } from "lodash"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocCastAsync } from "../../../fields/Doc"; +import { Doc, DocCastAsync, Opt, StrListCast } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { NumCast, StrCast, BoolCast, Cast } from "../../../fields/Types"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; +import { ViewSpecPrefix } from "../nodes/DocumentView"; import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { CollectionSubView } from "./CollectionSubView"; @@ -19,7 +24,6 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; import React = require("react"); -import { DocUtils } from "../../documents/Documents"; @observer export class CollectionTimeView extends CollectionSubView(doc => doc) { @@ -28,15 +32,63 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { @observable _collapsed: boolean = false; @observable _childClickedScript: Opt<ScriptField>; @observable _viewDefDivClick: Opt<ScriptField>; + @observable _focusDocFilters: Opt<string[]>; // fields that get overridden by a focus anchor + @observable _focusPivotField: Opt<string>; + @observable _focusRangeFilters: Opt<string[]>; + + getAnchor = () => { + const anchor = Docs.Create.TextanchorDocument({ + title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, + useLinkSmallAnchor: true, + hideLinkButton: true, + annotationOn: this.rootDoc + }); + + // save view spec information for anchor + const proto = Doc.GetProto(anchor); + proto.pivotField = this.pivotField; + proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]); + proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]); + proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; + + // store anchor in annotations list of document (not technically needed since these anchors are never drawn) + if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor); + } else { + this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]); + } + return anchor; + } + async componentDidMount() { - const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.props.Document.type), ""); - const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + this.props.setContentView?.(this); + const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); + ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + const childText = "openInLightbox(self, shiftKey); "; runInAction(() => { this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! }); this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); }); } + get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); } + @action + setViewSpec = (anchor: Doc, preview: boolean) => { + if (preview) { // if in preview, then override document's fields with view spec + this._focusPivotField = StrCast(anchor.pivotField); + this._focusDocFilters = StrListCast(anchor.docFilters); + this._focusRangeFilters = StrListCast(anchor.docRangeFilters); + } else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec + this.layoutDoc._prevFilterIndex = 1; + this.layoutDoc._pivotField = StrCast(anchor.pivotField); + this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters)); + this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters)); + } + return 0; + } + + pivotDocFilters = () => this._focusDocFilters || this.props.docFilters(); + pivotDocRangeFilters = () => this._focusRangeFilters || this.props.docRangeFilters(); layoutEngine = () => this._layoutEngine; toggleVisibility = action(() => this._collapsed = !this._collapsed); @@ -69,24 +121,31 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { }), returnFalse, emptyFunction); } - contentsDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex); - if (prevFilterIndex > 0) { - prevFilterIndex--; - this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField); - this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); - this.props.Document._prevFilterIndex = prevFilterIndex; - } else { - this.props.Document._docFilters = new List([]); - } - }), false); + goTo = (prevFilterIndex: number) => { + this.layoutDoc._pivotField = this.layoutDoc["_prevPivotFields" + prevFilterIndex]; + this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocFilter" + prevFilterIndex] as ObjectField); + this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); + this.layoutDoc._prevFilterIndex = prevFilterIndex; + } + + @action + contentsDown = (e: React.MouseEvent) => { + let prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex); + if (prevFilterIndex > 0) { + this.goTo(prevFilterIndex - 1); + } else { + this.layoutDoc._docFilters = new List([]); + } } @computed get contents() { - return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%", pointerEvents: this.props.active() ? undefined : "none" }} onPointerDown={this.contentsDown}> + return <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.active() ? undefined : "none" }} + onClick={this.contentsDown}> <CollectionFreeFormView {...this.props} - fitContentsToDoc={true} + engineProps={{ pivotField: this.pivotField, docFilters: this.docFilters, docRangeFilters: this.docRangeFilters }} + fitContentsToDoc={returnTrue} + docFilters={this.pivotDocFilters} + docRangeFilters={this.pivotDocRangeFilters} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} childFreezeDimensions={true} @@ -100,7 +159,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { } specificMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; - const doc = this.props.Document; + const doc = this.layoutDoc; layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" }); layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" }); @@ -123,10 +182,11 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof (pair.layout[fieldKey]) === "number" || - typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey))); + typeof (pair.layout[fieldKey]) === "boolean" || + typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey))); Array.from(keySet).map(fieldKey => - docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" })); - docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" }); + docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" })); + docItems.push({ description: ":(null)", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" }); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); ContextMenu.Instance.displayMenu(x, y, ":"); @@ -137,13 +197,13 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { GetValue: () => "", SetValue: (value: any) => { if (value?.length) { - this.props.Document._pivotField = value; + this.layoutDoc._pivotField = value; return true; } return false; }, showMenuOnLoad: true, - contents: ":" + StrCast(this.props.Document._pivotField), + contents: ":" + StrCast(this.layoutDoc._pivotField), toggle: this.toggleVisibility, color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; }; @@ -155,12 +215,12 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { render() { let nonNumbers = 0; this.childDocs.map(doc => { - const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)]))); + const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField]))); if (Number.isNaN(num)) { nonNumbers++; } }); - const forceLayout = StrCast(this.props.Document._forceRenderEngine); + const forceLayout = StrCast(this.layoutDoc._forceRenderEngine); const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; if (doTimeline !== (this._layoutEngine === "timeline")) { if (!this._changing) { @@ -173,7 +233,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { } return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu} - style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> + style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.layoutDoc._chromeStatus === "enabled" ? 51 : 0}px)` }}> {this.pivotKeyUI} {this.contents} {!this.props.isSelected() || !doTimeline ? (null) : <> @@ -189,10 +249,17 @@ Scripting.addGlobal(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBoun let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotDoc._pivotField; pivotDoc._prevFilterIndex = ++prevFilterIndex; runInAction(() => { pivotDoc._docFilters = new List(); - (bounds.payload as string[]).map(filterVal => - Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check")); + const filterVals = (bounds.payload as string[]); + filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check")); + const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); + if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { + if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { + pivotDoc._pivotField = filterVals[0]; + } + } }); });
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a1be6d8f2..564b41a89 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -141,8 +141,11 @@ export class height={"auto"} GetValue={() => StrCast(this.dataDoc.title)} SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { - if (this.props.Document.treeViewOutlineMode && enter) { - this.makeTextCollection(childDocs); + if (enter) { + switch (this.props.Document.treeViewType) { + case "outline": this.makeTextCollection(childDocs); break; + case "fileSystem": break; + } } return Doc.SetInPlace(this.dataDoc, "title", value, false); })} />; @@ -210,7 +213,7 @@ export class } @computed get titleBar() { const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; - return hideTitle ? (null) : (this.doc.treeViewOutlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); + return hideTitle ? (null) : (this.doc.treeViewType === "outline" ? this.documentTitle : this.editableTitle)(this.treeChildren); } render() { TraceMobx(); diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index d4b4cf333..53801eef1 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -16,7 +16,7 @@ import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { GetEffectiveAcl } from "../../../fields/util"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse } from "../../../Utils"; -import { Docs, DocumentOptions } from "../../documents/Documents"; +import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; @@ -575,7 +575,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { layerProvider={undefined} docViewPath={returnEmptyDoclist} freezeDimensions={true} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} renderDepth={this.props.renderDepth} rootSelected={() => false} PanelWidth={() => 150} diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e1e1c8656..c1a1a6f04 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -14,6 +14,7 @@ import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; +import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; @@ -23,7 +24,7 @@ import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { LightboxView } from '../LightboxView'; -import { DocAfterFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; +import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import { FieldViewProps } from '../nodes/FieldView'; import { PinProps, PresBox, PresMovement } from '../nodes/PresBox'; import { DefaultLayerProvider, DefaultStyleProvider, StyleLayers, StyleProp } from '../StyleProvider'; @@ -206,9 +207,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { const fieldKey = CollectionDockingView.Instance.props.fieldKey; const sublists = DocListCast(dview[fieldKey]); const tabs = Cast(sublists[0], Doc, null); - const tabdocs = await DocListCastAsync(tabs.data); + const tabdocs = await DocListCastAsync(tabs?.data); runInAction(() => { - if (!tabdocs?.includes(curPres)) { + if (!pinProps?.hidePresBox && !tabdocs?.includes(curPres)) { tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs CollectionDockingView.AddSplit(curPres, "right"); } @@ -278,7 +279,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { case "close": return CollectionDockingView.CloseSplit(doc, locationParams); case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); - case "lightbox": return runInAction(() => LightboxView.LightboxDoc = doc) ? true : false; + case "lightbox": { + // TabDocView.PinDoc(doc, { hidePresBox: true }); + return LightboxView.AddDocTab(doc, location); + } case "inPlace": case "add": default: @@ -333,7 +337,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { ScreenToLocalTransform={Transform.Identity} renderDepth={0} whenActiveChanged={emptyFunction} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} styleProvider={TabDocView.miniStyleProvider} layerProvider={undefined} addDocTab={this.addDocTab} @@ -341,7 +345,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { docFilters={CollectionDockingView.Instance.docFilters} docRangeFilters={CollectionDockingView.Instance.docRangeFilters} searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} - fitContentsToDoc={true} + fitContentsToDoc={returnTrue} /> <div className="miniOverlay" onPointerDown={this.miniDown} > <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} /> @@ -356,11 +360,25 @@ export class TabDocView extends React.Component<TabDocViewProps> { </Tooltip> </>; } - focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => { + @action + focusFunc = (doc: Doc, options?: DocFocusOptions) => { + const vals = (!options?.originalTarget || options?.originalTarget === this._document) && this.view?.ComponentView?.freeformData?.(true); + if (vals && this._document) { + const focusSpeed = 1000; + this._document._panX = vals.panX; + this._document._panY = vals.panY; + this._document._viewScale = vals.scale; + this._document._viewTransition = `transform ${focusSpeed}ms`; + setTimeout(action(() => { + this._document!._viewTransition = undefined; + options?.afterFocus?.(false); + }), focusSpeed); + } else { + options?.afterFocus?.(false); + } if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) } - afterFocus?.(false); } active = () => this._isActive; ScreenToLocalTransform = () => { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 0d89c7b43..f3def9a23 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -93,14 +93,15 @@ export class TreeView extends React.Component<TreeViewProps> { get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive get treeViewLockExpandedView() { return this.doc.treeViewLockExpandedView; } - get defaultExpandedView() { return StrCast(this.doc.treeViewDefaultExpandedView, this.noviceMode || this.outlineMode ? "layout" : "fields"); } - get treeViewDefaultExpandedView() { return this.treeViewLockExpandedView ? this.defaultExpandedView : (this.childDocs ? this.fieldKey : this.defaultExpandedView); } + get defaultExpandedView() { return StrCast(this.doc.treeViewDefaultExpandedView, this.noviceMode || this.outlineMode || this.fileSysMode ? "layout" : "fields"); } + get treeViewDefaultExpandedView() { return this.treeViewLockExpandedView ? this.defaultExpandedView : (this.childDocs && !this.fileSysMode ? this.fieldKey : this.defaultExpandedView); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get outlineMode() { return this.props.treeView.doc.treeViewOutlineMode; } + @computed get outlineMode() { return this.props.treeView.doc.treeViewType === "outline"; } + @computed get fileSysMode() { return this.props.treeView.doc.treeViewType === "fileSystem"; } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.treeViewDefaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @@ -108,6 +109,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get layoutDoc() { return Doc.Layout(this.doc); } @computed get fieldKey() { TraceMobx(); const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { + if (this.fileSysMode && !this.doc.isFolder) return [] as Doc[]; const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields @@ -133,13 +135,14 @@ export class TreeView extends React.Component<TreeViewProps> { } @undoBatch @action removeDoc = (doc: Doc | Doc[]) => this.remove(doc, Doc.LayoutFieldKey(this.doc)); + selected = () => SelectionManager.Views().length && SelectionManager.Views()[0].props.Document === this.props.document; constructor(props: any) { super(props); - const titleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); - const openScript = ScriptField.MakeScript(`openOnRight(self)`); - const treeOpenScript = ScriptField.MakeScript(`self.treeViewOpen = !self.treeViewOpen`); - this._editTitleScript = !Doc.IsSystem(this.props.document) ? titleScript && (() => titleScript) : treeOpenScript && (() => treeOpenScript); - this._openScript = !Doc.IsSystem(this.props.document) ? openScript && (() => openScript) : undefined; + const titleScript = ScriptField.MakeScript(`{scriptContext.selected() && setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { scriptContext: "any", documentView: "any" }); + const openScript = ScriptField.MakeScript(`self.isFolder? (scriptContext.treeViewOpen = !scriptContext.treeViewOpen) : openOnRight(self) && documentView.select()`, { scriptContext: "any", documentView: "any" }); + const treeOpenScript = ScriptField.MakeScript(`scriptContext.treeViewOpen = !scriptContext.treeViewOpen`, { scriptContext: "any" }); + this._editTitleScript = !Doc.IsSystem(this.props.document) || props.document.isFolder ? titleScript && (() => titleScript) : treeOpenScript && (() => treeOpenScript); + this._openScript = !Doc.IsSystem(this.props.document) || props.document.isFolder ? openScript && (() => openScript) : undefined; if (Doc.GetT(this.props.document, "editTitle", "string", true) === "*") Doc.SetInPlace(this.props.document, "editTitle", this._uniqueId, false); } @@ -192,7 +195,7 @@ export class TreeView extends React.Component<TreeViewProps> { const bullet = Docs.Create.TextDocument("-text-", { layout: CollectionView.LayoutString("data"), title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, - _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, + _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline", x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); @@ -209,6 +212,13 @@ export class TreeView extends React.Component<TreeViewProps> { bullet.context = this.props.treeView.Document; return added; } + makeFolder = () => { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + const folder = Docs.Create.FreeformDocument([], { title: "-folder-", _stayInCollection: true, isFolder: true, system: true }); + const added = this.props.addDocument(folder); + folder.context = this.props.treeView.Document; + return added; + } editableView = (key: string, style?: string) => (<EditableView oneLine={true} @@ -220,10 +230,17 @@ export class TreeView extends React.Component<TreeViewProps> { fontStyle={style} fontSize={12} GetValue={() => StrCast(this.doc[key])} + OnFillDown={(value) => { + if (this.fileSysMode) { + this.makeFolder(); + } + }} SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => { Doc.SetInPlace(this.doc, key, value, false); if (this.outlineMode && enterKey) { this.makeTextCollection(); + } else if (this.fileSysMode && enterKey) { + // add folder } else { Doc.SetInPlace(this.doc, "editTitle", undefined, false); } @@ -232,7 +249,7 @@ export class TreeView extends React.Component<TreeViewProps> { SelectionManager.DeselectAll(); return false; }} - OnEmpty={undoBatch(() => this.props.treeView.doc.treeViewOutlineMode && this.props.removeDoc?.(this.doc))} + OnEmpty={undoBatch(() => this.outlineMode && this.props.removeDoc?.(this.doc))} OnTab={undoBatch((shift?: boolean) => { shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", `${this.props.treeView._uniqueId}`, false), 0); @@ -249,7 +266,7 @@ export class TreeView extends React.Component<TreeViewProps> { const pt = [de.x, de.y]; const rect = this._header!.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); + const inside = this.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceDocument; const destDoc = this.doc; @@ -443,15 +460,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { TraceMobx(); - - const iconType = Doc.toIcon(this.doc); - - // console.log(this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon)); <- always undefined - //const iconType = this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon); - - - // const footerDecoration = this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decoration + ":footer"); - + const iconType = this.doc.isFolder ? (this.treeViewOpen ? "chevron-down" : "chevron-right") : Doc.toIcon(this.doc); const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} key={"bullet"} @@ -484,7 +493,9 @@ export class TreeView extends React.Component<TreeViewProps> { <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { - if (this.treeViewOpen) { + if (this.fileSysMode) { + this.doc.treeViewExpandedView = this.doc.isFolder ? this.fieldKey : this.treeViewExpandedView === "layout" ? "fields" : "layout"; + } else if (this.treeViewOpen) { this.doc.treeViewExpandedView = this.treeViewLockExpandedView ? this.doc.treeViewExpandedView : this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields") : this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : @@ -517,7 +528,7 @@ export class TreeView extends React.Component<TreeViewProps> { switch (property.split(":")[0]) { case StyleProp.Opacity: return this.outlineMode ? undefined : 1; - case StyleProp.BackgroundColor: return StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); + case StyleProp.BackgroundColor: return this.selected() ? "#7089bb" : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); case StyleProp.DocContents: return testDocProps(props) && !props?.treeViewDoc ? (null) : <div className="treeView-label" style={{ // just render a title for a tree view label (identified by treeViewDoc being set in 'props') maxWidth: props?.PanelWidth() || undefined, @@ -556,6 +567,7 @@ export class TreeView extends React.Component<TreeViewProps> { ref={this._docRef} Document={this.doc} DataDoc={undefined} + scriptContext={this} styleProvider={this.titleStyleProvider} layerProvider={undefined} docViewPath={returnEmptyDoclist} @@ -640,7 +652,7 @@ export class TreeView extends React.Component<TreeViewProps> { PanelHeight={panelHeight} NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} - fitContentsToDoc={true} + fitContentsToDoc={returnTrue} hideTitle={asText} LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined} focus={asText ? this.refocus : returnFalse} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index edf473a90..afc1babeb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,16 +1,12 @@ -import { Doc, Field, FieldResult, WidthSym, HeightSym } from "../../../../fields/Doc"; -import { NumCast, StrCast, Cast } from "../../../../fields/Types"; -import { ScriptBox } from "../../ScriptBox"; -import { CompileScript } from "../../../util/Scripting"; -import { ScriptField } from "../../../../fields/ScriptField"; -import { OverlayView, OverlayElementOptions } from "../../OverlayView"; -import { emptyFunction, aggregateBounds } from "../../../../Utils"; -import React = require("react"); +import { Doc, Field, FieldResult, HeightSym, WidthSym } from "../../../../fields/Doc"; import { Id, ToString } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { RefField } from "../../../../fields/RefField"; import { listSpec } from "../../../../fields/Schema"; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { aggregateBounds } from "../../../../Utils"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import React = require("react"); export interface ViewDefBounds { type: string; @@ -89,7 +85,8 @@ export function computerPassLayout( pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], + engineProps: any ) { const docMap = new Map<string, PoolData>(); childPairs.forEach(({ layout, data }, i) => { @@ -110,7 +107,8 @@ export function computerStarburstLayout( pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], + engineProps: any ) { const docMap = new Map<string, PoolData>(); const burstRadius = [NumCast(pivotDoc._starburstRadius, panelDim[0]), NumCast(pivotDoc._starburstRadius, panelDim[1])]; @@ -137,16 +135,17 @@ export function computePivotLayout( pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], + engineProps: any ) { const docMap = new Map<string, PoolData>(); const fieldKey = "data"; const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>(); let nonNumbers = 0; - const pivotFieldKey = toLabel(pivotDoc._pivotField); + const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField); childPairs.map(pair => { - const lval = pivotFieldKey === "#" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) : + const lval = pivotFieldKey === "#" || pivotFieldKey === "tags" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) : Cast(pair.layout[pivotFieldKey], listSpec("string"), null); const num = toNumber(pair.layout[pivotFieldKey]); @@ -278,7 +277,8 @@ export function computeTimelineLayout( pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], - viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], + engineProps?: any ) { const fieldKey = "data"; const pivotDateGroups = new Map<number, Doc[]>(); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bba0807a4..58288a7b1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,22 +1,24 @@ -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from "../../../../fields/Doc"; +import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; +import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; -import { createSchema, makeInterface, listSpec } from "../../../../fields/Schema"; +import { createSchema, listSpec, makeInterface } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal, returnTrue } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, setupMoveUpEvents, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; @@ -30,13 +32,16 @@ import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; +import { DocumentDecorations } from "../../DocumentDecorations"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; -import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewProps } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; -import { DocumentViewProps, DocAfterFocusFunc, DocumentView } from "../../nodes/DocumentView"; +import { LightboxView } from "../../LightboxView"; +import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; +import { FieldViewProps } from "../../nodes/FieldView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import { PresBox } from "../../nodes/PresBox"; +import { StyleLayers, StyleProp } from "../../StyleProvider"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionViewType } from "../CollectionView"; @@ -45,11 +50,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { StyleProp, StyleLayers } from "../../StyleProvider"; -import { DocumentDecorations } from "../../DocumentDecorations"; -import { FieldViewProps } from "../../nodes/FieldView"; -import { reset } from "colors"; export const panZoomSchema = createSchema({ _panX: "number", @@ -79,6 +79,7 @@ export type collectionFreeformViewProps = { parentActive: (outsideReaction: boolean) => boolean; scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) + engineProps?: any; }; @observer @@ -114,10 +115,20 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); + @observable _focusFilters: Opt<string[]>; // fields that get overriden by focus anchor + @observable _focusRangeFilters: Opt<string[]>; // fields that get overriden by focus anchor @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.active() || this.props.active()); } - @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } - @computed get fitToContent() { return (this.props.fitContentsToDoc || this.Document._fitToBox) && !this.isAnnotationOverlay; } + @computed get fitToContentVals() { + return { + panX: (this.contentBounds.x + this.contentBounds.r) / 2, + panY: (this.contentBounds.y + this.contentBounds.b) / 2, + scale: !this.childDocs.length ? 1 : + Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), + this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) + } || undefined; + } + @computed get fitToContent() { return (this.props.fitContentsToDoc?.() || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); } @@ -125,17 +136,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; - private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; - private zoomScaling = () => { - const mult = this.fitToContentScaling / this.parentScaling; - if (this.fitToContent) { - const zs = !this.childDocs.length ? 1 : - Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); - return mult * zs; - } - return mult * NumCast(this.Document[this.scaleFieldKey], 1); - } + private panX = () => this.freeformData()?.panX ?? NumCast(this.Document._panX); + private panY = () => this.freeformData()?.panY ?? NumCast(this.Document._panY); + private zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1)) / this.parentScaling; @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / this.parentScaling / scaling; // shift so pan position is at center of window for non-overlay collections @@ -441,7 +444,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return; } this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - if (e.button === 0 && (!e.shiftKey || this._hitCluster !== -1) && !e.altKey && !e.ctrlKey && this.props.active(true)) { + if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -606,12 +609,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onClick = (e: React.MouseEvent) => { - if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { - if (Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a double click - runInAction(() => DocumentLinksButton.StartLink = DocumentLinksButton.StartLinkView = undefined); - this.scaleAtPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); - e.stopPropagation(); - e.preventDefault(); + if ((Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { + if (Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a double click + if (e.shiftKey) { + if (this.layoutDoc.targetScale) { + this.scaleAtPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); + } + e.stopPropagation(); + e.preventDefault(); + } } this._lastTap = Date.now(); } @@ -811,7 +817,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode) return; + if (this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode === "outline") return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -855,13 +861,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2; } } - if (!this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.Document)) { + if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || CurrentUserUtils.OverlayDocs.includes(this.Document)) { this.Document._viewTransition = panType; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); - this.Document._panX = this.isAnnotationOverlay ? newPanX : panX; - this.Document._panY = this.isAnnotationOverlay ? newPanY : panY; + !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX); + !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY); } } @@ -894,7 +900,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; } - focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { + focusDocument = (doc: Doc, options?: DocFocusOptions) => { const state = HistoryUtil.getState(); // TODO This technically isn't correct if type !== "doc", as @@ -910,37 +916,42 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P HistoryUtil.pushState(state); } } + const LightboxState = LightboxView.GetSavedState(this.props.Document); SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { - this.props.focus(doc, undefined, undefined, afterFocus); + this.props.focus(doc, options); } else { - const xfToCollection = docTransform ?? Transform.Identity(); + const xfToCollection = options?.docTransform ?? Transform.Identity(); const layoutdoc = Doc.Layout(doc); - const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; + const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey], transition: this.Document._viewTransition }; const newState = HistoryUtil.getState(); - const cantTransform = this.props.isAnnotationOverlay || this.rootDoc._isGroup; - const { px, py } = cantTransform ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, willZoom ? scale || .75 : undefined); + const cantTransform = this.props.isAnnotationOverlay || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); + const { panX, panY, scale } = cantTransform ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection - newState.initializers![this.Document[Id]] = { panX: px, panY: py }; + newState.initializers![this.Document[Id]] = { panX: panX, panY: panY }; HistoryUtil.pushState(newState); } // focus on the document in the collection - const didMove = !doc.z && (px !== savedState.px || py !== savedState.py); + const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== undefined); const focusSpeed = didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... - if (didMove) this.setPan(px, py, `transform ${focusSpeed}ms`, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow - Doc.BrushDoc(this.rootDoc); - !doc.hidden && Doc.linkFollowHighlight(doc); + if (didMove) { + this.setPan(panX, panY, `transform ${focusSpeed}ms`, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + scale && (this.Document[this.scaleFieldKey] = scale); + } + Doc.BrushDoc(doc); + const startTime = Date.now(); // focus on this collection within its parent view. the parent view after focusing determines whether to reset the view change within the collection const endFocus = async (moved: boolean) => { doc.hidden && Doc.UnHighlightDoc(doc); - const resetView = afterFocus ? await afterFocus(moved) : false; + const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing; if (resetView) { - this.Document._panX = savedState.px; - this.Document._panY = savedState.py; - this.Document[this.scaleFieldKey] = savedState.s; - this.Document._viewTransition = savedState.pt; + const restoreState = LightboxView.LightboxDoc !== this.props.Document && LightboxState ? LightboxState : savedState; + this.Document._panX = restoreState.panX; + this.Document._panY = restoreState.panY; + this.Document[this.scaleFieldKey] = restoreState.scale; + this.Document._viewTransition = restoreState.transition; } return resetView; }; @@ -950,8 +961,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P : new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1); - this.props.focus(cantTransform ? doc : this.rootDoc, willZoom, scale, (didFocus: boolean) => - new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)), xf.transform(docTransform ?? Transform.Identity())); + + this.props.focus(cantTransform ? doc : this.rootDoc, { + ...options, docTransform: xf, + afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => + setTimeout(async () => res(await endFocus(didMove || didFocus)), Math.max(0, focusSpeed - (Date.now() - startTime)))) + }); } } @@ -961,17 +976,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); const pt2 = xf.transformPoint(NumCast(doc.x) + doc[WidthSym](), NumCast(doc.y) + doc[HeightSym]()); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] }; + const cx = NumCast(this.layoutDoc._panX); + const cy = NumCast(this.layoutDoc._panY); + const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; if (scale) { - this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / Math.abs(pt2[0] - pt[0])), this.props.PanelHeight() / Math.abs(pt2[1] - pt[1]); - return { px: (bounds.left + bounds.right) / 2, py: (bounds.top + bounds.bot) / 2 }; + const maxZoom = 2; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context + + return { + panX: (bounds.left + bounds.right) / 2, + panY: (bounds.top + bounds.bot) / 2, + scale: Math.min(maxZoom, scale * Math.min(this.props.PanelWidth() / Math.abs(pt2[0] - pt[0]), this.props.PanelHeight() / Math.abs(pt2[1] - pt[1]))) + }; + } else if ((screen.right - screen.left) < (bounds.right - bounds.left) || + (screen.bot - screen.top) < (bounds.bot - bounds.top)) { + return { + panX: (bounds.left + bounds.right) / 2, + panY: (bounds.top + bounds.bot) / 2, + scale: Math.min(this.props.PanelHeight() / (bounds.bot - bounds.top), this.props.PanelWidth() / (bounds.right - bounds.left)) / 1.1 + }; } else { - const cx = NumCast(this.layoutDoc._panX); - const cy = NumCast(this.layoutDoc._panY); - const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; return { - px: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - py: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot) + panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), + panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), }; } } @@ -1000,8 +1027,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], - docFilters: this.docFilters, - docRangeFilters: this.docRangeFilters, + docFilters: this.freeformDocFilters, + docRangeFilters: this.freeformRangeDocFilters, searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, styleProvider: this.getClusterColor, @@ -1099,9 +1126,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pivotDoc: Doc, childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], - viewDefsToJSX: ((views: ViewDefBounds[]) => ViewDefResult[])) => ViewDefResult[] + viewDefsToJSX: ((views: ViewDefBounds[]) => ViewDefResult[]), + engineProps: any) => ViewDefResult[] ) { - return engine(poolData, this.props.Document, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); + return engine(poolData, this.props.Document, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX, this.props.engineProps); } doFreeformLayout(poolData: Map<string, PoolData>) { @@ -1179,9 +1207,42 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return elements; } + freeformDocFilters = () => this._focusFilters || this.docFilters(); + freeformRangeDocFilters = () => this._focusRangeFilters || this.docRangeFilters(); + @action + setViewSpec = (anchor: Doc, preview: boolean) => { + if (preview) { + this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters); + this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters); + } else if (anchor.pivotField !== undefined) { + this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters)); + this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters)); + } + return 0; + } + + getAnchor = () => { + const anchor = Docs.Create.TextanchorDocument({ + title: StrCast(this.layoutDoc._viewType), + useLinkSmallAnchor: true, + hideLinkButton: true, + annotationOn: this.rootDoc + }); + const proto = Doc.GetProto(anchor); + proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; + proto[ViewSpecPrefix + "_docFilters"] = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]); + if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor); + } else { + this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]); + } + return anchor; + } + freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined; @action componentDidMount() { super.componentDidMount?.(); + this.props.setContentView?.(this); this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index f7fb2b83d..f5a60effe 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -132,6 +132,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); })(); e.stopPropagation(); + } else if (e.key === "f" && e.ctrlKey) { + e.preventDefault(); + const root = Docs.Create.FreeformDocument([], { title: "folder", _stayInCollection: true, system: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([root], { title: "root", isFolder: true, treeViewType: "fileSystem", treeViewTruncateTitleWidth: 150 }); + Doc.GetProto(folder).isFolder = true; + folder.x = x; + folder.y = y; + folder._width = 200; + folder._height = 300; + this.props.addDocument?.(folder); + //setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(slide)!, false)); + e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { // e.preventDefault(); // navigator.clipboard.readText().then(text => { 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; } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index cd32c2c3a..4e3721e2b 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -9,42 +9,30 @@ import { LinkManager } from "../../util/LinkManager"; import { undoBatch } from "../../util/UndoManager"; import "./Annotation.scss"; import { AnchorMenu } from "./AnchorMenu"; +import { FieldViewProps, FieldView } from "../nodes/FieldView"; -interface IAnnotationProps { +interface IAnnotationProps extends FieldViewProps { anno: Doc; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc, unpin?: boolean) => void; - focus: (doc: Doc) => void; - select: (isCtrlPressed: boolean) => void; dataDoc: Doc; fieldKey: string; showInfo: (anno: Opt<Doc>) => void; } - @observer export class Annotation extends React.Component<IAnnotationProps> { render() { return DocListCast(this.props.anno.annotations).map(a => - <RegionAnnotation {...this.props} showInfo={this.props.showInfo} select={this.props.select} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); + <RegionAnnotation {...this.props} showInfo={this.props.showInfo} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); } } -interface IRegionAnnotationProps { - anno: Doc; +interface IRegionAnnotationProps extends IAnnotationProps { x: number; y: number; width: number; height: number; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc, unpin: boolean) => void; - select: (isCtrlPressed: boolean) => void; document: Doc; - dataDoc: Doc; - fieldKey: string; - showInfo: (anno: Opt<Doc>) => void; } - @observer class RegionAnnotation extends React.Component<IRegionAnnotationProps> { private _reactionDisposer?: IReactionDisposer; @@ -90,8 +78,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { @undoBatch pinToPres = () => { const group = FieldValue(Cast(this.props.document.group, Doc)); - const isPinned = group && Doc.isDocPinned(group) ? true : false; - group && this.props.pinToPres(group, isPinned); + group && this.props.pinToPres(group); } @undoBatch @@ -115,13 +102,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); e.stopPropagation(); } - else if (e.button === 0) { - e.persist(); + else if (e.button === 0 && this.props.document.group instanceof Doc) { e.stopPropagation(); - PromiseValue(this.props.document.group).then(annoGroup => annoGroup instanceof Doc && - LinkManager.traverseLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined, - () => this.props.select(false)) - ); + LinkManager.FollowLink(undefined, this.props.document.group, this.props, false); } } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index dd9dfa733..7687690b2 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -28,7 +28,7 @@ import { AnchorMenu } from "./AnchorMenu"; import "./PDFViewer.scss"; const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import React = require("react"); -import { DocAfterFocusFunc } from "../nodes/DocumentView"; +import { LinkDocPreview } from "../nodes/LinkDocPreview"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); const _global = (window /* browser */ || global /* node */) as any; @@ -88,6 +88,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _lastSearch = false; private _viewerIsSetup = false; private _ignoreScroll = false; + private _initialScroll: Opt<number>; private _smoothScrolling = true; @computed get allAnnotations() { @@ -180,17 +181,21 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location, // otherwise it will scroll smoothly. - scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => { + scrollFocus = (doc: Doc, smooth: boolean) => { const mainCont = this._mainCont.current; - if (doc !== this.rootDoc && mainCont) { + let focusSpeed: Opt<number>; + if (doc !== this.rootDoc && mainCont && this._pdfViewer) { const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { - if (smooth) smoothScroll(500, mainCont, scrollTo); - else mainCont.scrollTop = scrollTo; - return afterFocus?.(true); + focusSpeed = 500; + + if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo); + else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } + } else { + this._initialScroll = NumCast(doc.y); } - afterFocus?.(false); + return focusSpeed; } @action @@ -221,7 +226,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this.gotoPage(this.Document._curPage || 1); } document.removeEventListener("pagesinit", this.pagesinit); - var quickScroll: string | undefined = ""; + var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : ""; this._disposers.scroll = reaction( () => NumCast(this.Document._scrollTop), (pos) => { @@ -248,6 +253,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu { fireImmediately: true } ); quickScroll = undefined; + if (this._initialScroll !== undefined && this._mainCont.current) { + this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) }); + this._initialScroll = undefined; + } } createPdfViewer() { @@ -312,7 +321,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu onScroll = (e: React.UIEvent<HTMLElement>) => { if (this._mainCont.current && !this._smoothScrolling) { this._ignoreScroll = true; - this.layoutDoc._scrollTop = this._mainCont.current.scrollTop; + if (!LinkDocPreview.LinkInfo) { + this.layoutDoc._scrollTop = this._mainCont.current.scrollTop; + } this._ignoreScroll = false; } } @@ -361,9 +372,11 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onPointerDown = (e: React.PointerEvent): void => { const hit = document.elementFromPoint(e.clientX, e.clientY); - if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation - e.button === 0 && e.stopPropagation(); - } + // 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. + // if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation + // e.button === 0 && e.stopPropagation(); + // } // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; @@ -374,6 +387,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (!e.altKey && e.button === 0 && this.active(true)) { if (e.target && ((e.target as any).className.includes("endOfContent") || ((e.target as any).parentElement.className !== "textLayer"))) { this._marqueeing = [e.clientX, e.clientY]; // if texLayer is hit, then we select text instead of using a marquee + document.addEventListener("pointermove", this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called } else { // clear out old marquees and initialize menu for new selection AnchorMenu.Instance.Status = "marquee"; @@ -381,8 +395,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._savedAnnotations.clear(); this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointermove", this.onSelectMove); } - document.addEventListener("pointermove", this.onSelectMove); } } @@ -456,7 +470,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { this._setPreviewCursor(e.clientX, e.clientY, false); } - e.stopPropagation(); + // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; @@ -491,7 +505,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu TraceMobx(); return <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.Document), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} showInfo={this.showInfo} select={this.props.select} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 01a55660a..c794a4132 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -22,6 +22,7 @@ import { PresBox, PresColor, PresMovement } from "../nodes/PresBox"; import { StyleProp } from "../StyleProvider"; import "./PresElementBox.scss"; import React = require("react"); +import { DocUtils } from "../../documents/Documents"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -105,7 +106,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc parentActive={this.props.active} moveDocument={this.props.moveDocument!} renderDepth={this.props.renderDepth + 1} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} whenActiveChanged={returnFalse} bringToFront={returnFalse} docFilters={this.props.docFilters} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7f6ab57e7..5e9ab4baa 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1148,7 +1148,7 @@ export namespace Doc { case DocumentType.IMG: return "image"; case DocumentType.COMPARISON: return "columns"; case DocumentType.RTF: return "sticky-note"; - case DocumentType.COL: return "folder"; + case DocumentType.COL: return !doc.isFolder ? "folder" : "chevron-right"; case DocumentType.WEB: return "globe-asia"; case DocumentType.SCREENSHOT: return "photo-video"; case DocumentType.WEBCAM: return "video"; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 056243953..6973079b0 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -20,6 +20,7 @@ export const documentSchema = createSchema({ activeFrame: "number", // the active frame of a frame based animated document _currentTimecode: "number", // current play back time of a temporal document (video / audio) _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + _timecodeToHIde: "number", // the time that a document should be hidden isLabel: "boolean", // whether the document is a label or not (video / audio) markers: listSpec(Doc), // list of markers for audio / video x: "number", // x coordinate when in a freeform view @@ -41,8 +42,7 @@ export const documentSchema = createSchema({ _yMargin: "number", // margin added on top/bottom of most documents to add separation from their container _overflow: "string", // sets overflow behvavior for CollectionFreeForm views _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document - _showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document - _showTitleHover: "string", // the showTitle should be shown only on hover + _showTitle: "string", // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover _showAudio: "boolean", // whether to show the audio record icon on documents _freeformLOD: "boolean", // whether to enable LOD switching for CollectionFreeFormViews _pivotField: "string", // specifies which field key should be used as the timeline/pivot axis @@ -81,7 +81,7 @@ export const documentSchema = createSchema({ treeViewLockExpandedView: "boolean", // whether the expanded view can be changed treeViewDefaultExpandedView: "string", // name of field whose contents are displayed by default treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) - treeViewOutlineMode: "boolean", // whether tree view is an outline and clicks edit document titles immediately since double-click opening is turned off + treeViewType: "string", // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off // interaction and linking properties ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 058c61a6d..da3dbecbe 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -13,7 +13,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from "react"; -import { Docs, DocumentOptions } from '../client/documents/Documents'; +import { Docs, DocumentOptions, DocUtils } from '../client/documents/Documents'; import { DocumentType } from "../client/documents/DocumentTypes"; import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; import { Scripting } from '../client/util/Scripting'; @@ -210,7 +210,7 @@ export class MobileInterface extends React.Component { PanelWidth={this.returnWidth} PanelHeight={this.returnHeight} renderDepth={0} - focus={emptyFunction} + focus={DocUtils.DefaultFocus} styleProvider={this.whitebackground} layerProvider={undefined} docViewPath={returnEmptyDoclist} |