aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/DocumentManager.ts
diff options
context:
space:
mode:
authorbrynnchernosky <56202540+brynnchernosky@users.noreply.github.com>2023-01-19 14:33:22 -0500
committerbrynnchernosky <56202540+brynnchernosky@users.noreply.github.com>2023-01-19 14:33:22 -0500
commit0ef7050b0792ce183c7d5cda637cb79b7a92b704 (patch)
treed1dca8f09ddc2954c2ce88439172aeded672c0b6 /src/client/util/DocumentManager.ts
parentceb338752aacc383c97a0e3a9b608365a1cf39b6 (diff)
parentd5f796b433d7e72130d4109a3775347ccb10c454 (diff)
Merge branch 'master' of github.com:brown-dash/Dash-Web into master
Diffstat (limited to 'src/client/util/DocumentManager.ts')
-rw-r--r--src/client/util/DocumentManager.ts296
1 files changed, 166 insertions, 130 deletions
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index b046d950f..70fe7f2c0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,19 +1,19 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { action, observable, ObservableSet, runInAction } from 'mobx';
+import { AnimationSym, Doc, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
+import { listSpec } from '../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
import { returnFalse } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
-import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
-import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
import { CollectionView } from '../views/collections/CollectionView';
+import { LightboxView } from '../views/LightboxView';
+import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
import { ScriptingGlobals } from './ScriptingGlobals';
import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
const { Howl } = require('howler');
export class DocumentManager {
@@ -31,26 +31,50 @@ export class DocumentManager {
//private constructor so no other class can create a nodemanager
private constructor() {}
+ private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+ public AddViewRenderedCb = (doc: Doc, func: (dv: DocumentView) => any) => {
+ const dv = this.getDocumentViewById(doc[Id]);
+ this._viewRenderedCbs.push({ doc, func });
+ if (dv) {
+ this.callAddViewFuncs(dv);
+ }
+ };
+ callAddViewFuncs = (view: DocumentView) => {
+ const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
+ if (callFuncs.length) {
+ this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc));
+ const intTimer = setInterval(
+ () => {
+ if (!view.ComponentView?.incrementalRendering?.()) {
+ callFuncs.forEach(cf => cf.func(view));
+ clearInterval(intTimer);
+ }
+ },
+ view.ComponentView?.incrementalRendering?.() ? 0 : 100
+ );
+ }
+ };
+
@action
public AddView = (view: DocumentView) => {
//console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1';
- DocListCast(view.rootDoc.links).forEach(link => {
- this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
- this.LinkedDocumentViews.push({
- a: viewAnchorIndex === 'anchor2' ? otherView : view,
- b: viewAnchorIndex === 'anchor2' ? view : otherView,
- l: link,
- })
- );
- });
+ const link = view.rootDoc;
+ this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+ this.LinkedDocumentViews.push({
+ a: viewAnchorIndex === 'anchor2' ? otherView : view,
+ b: viewAnchorIndex === 'anchor2' ? view : otherView,
+ l: link,
+ })
+ );
this.LinkAnchorBoxViews.push(view);
// this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " +
// view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString));
} else {
this.DocumentViews.add(view);
}
+ this.callAddViewFuncs(view);
};
public RemoveView = action((view: DocumentView) => {
this.LinkedDocumentViews.slice().forEach(
@@ -122,7 +146,14 @@ export class DocumentManager {
}
public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
- return this.getDocumentViewById(toFind[Id], preferredCollection);
+ const found =
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFind;
+ return this.getDocumentViewById(found[Id], preferredCollection);
}
public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
@@ -131,10 +162,19 @@ export class DocumentManager {
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 => {
+ if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc);
const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc);
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 getDocumentViews(toFind: Doc): DocumentView[] {
+ public getDocumentViews(toFindIn: Doc): DocumentView[] {
+ const toFind =
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFindIn;
+
const toReturn: DocumentView[] = [];
const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath));
@@ -150,26 +190,39 @@ export class DocumentManager {
return toReturn;
}
+ static playAudioAnno(doc: Doc) {
+ const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
+ if (anno) {
+ if (anno instanceof AudioField) {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ });
+ }
+ }
+ }
+
+ public static removeOverlayViews() {
+ DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined)));
+ DocumentManager._overlayViews?.clear();
+ }
+ static _overlayViews = new ObservableSet<DocumentView>();
static addView = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, 'right');
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
finished?.();
};
public jumpToDocument = (
targetDoc: Doc, // document to display
- willZoom: boolean, // whether to zoom doc to take up most of screen
+ options: DocFocusOptions, // options for how to navigate to target
createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
- docContext: Doc[], // context to load that should contain the target
- linkDoc?: Doc, // link that's being followed
- 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,
- noSelect?: boolean,
- presZoomScale?: number
+ docContextPath: Doc[], // context to load that should contain the target
+ finished?: () => void
): void => {
- originalTarget = originalTarget ?? targetDoc;
- const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
- const docView = getFirstDocView(targetDoc, originatingDoc);
+ const originalTarget = options.originalTarget ?? targetDoc;
+ const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc);
const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
var wasHidden = resolvedTarget.hidden;
@@ -179,65 +232,63 @@ export class DocumentManager {
docView?.props.bringToFront(resolvedTarget);
});
}
- const focusAndFinish = (didFocus: boolean) => {
+ const focusAndFinish = action((didFocus: boolean) => {
const finalTargetDoc = resolvedTarget;
- if (originatingDoc?.isPushpin) {
+ if (options.toggleTarget) {
if (!didFocus && !wasHidden) {
// don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
finalTargetDoc.hidden = !finalTargetDoc.hidden;
}
} else {
finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
- !noSelect && docView?.select(false);
- if (originatingDoc?.followLinkAudio) {
- const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement();
- if (anno) {
- if (anno instanceof AudioField) {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- });
- }
- }
+ !options.noSelect && docView?.select(false);
+ }
+ if (targetDoc.textHtml && options.zoomTextSelections) {
+ const containerView = DocumentManager.Instance.getFirstDocumentView(finalTargetDoc);
+ if (containerView) {
+ containerView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+ containerView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+ DocumentManager._overlayViews.add(containerView);
+ if (Doc.UnhighlightTimer) {
+ Doc.AddUnHighlightWatcher(() => {
+ DocumentManager.removeOverlayViews();
+ containerView.htmlOverlayEffect = '';
+ });
+ } else setTimeout(() => (containerView.htmlOverlayEffect = ''));
}
}
finished?.();
- };
- const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
- const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
- const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
- const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
- const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
+ });
+ const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
if (annoContainerView) {
if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
- annoContainerView.iconify(() =>
- annoContainerView.focus(targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoomScale,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- })
- );
- return;
- } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
+ return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
+ }
+ if (!docView && targetDoc.type !== DocumentType.MARKER) {
annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
}
}
+
+ const contextDoc = docContextPath.length ? docContextPath[0] : undefined;
+ const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : [];
+ const targetDocContext = contextDoc || annotatedDoc;
+ const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
+ const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
if (focusView) {
- !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
+ if (focusView.rootDoc === originalTarget) {
+ if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
+ else {
+ focusView.rootDoc[AnimationSym] = options.effect;
+ if (Doc.UnhighlightTimer) {
+ Doc.AddUnHighlightWatcher(action(() => (focusView.rootDoc[AnimationSym] = undefined)));
+ }
+ }
+ }
+ if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
const doFocus = (forceDidFocus: boolean) =>
- focusView.focus(originalTarget ?? targetDoc, {
+ focusView.focus(originalTarget, {
+ ...options,
originalTarget,
- willZoom,
- scale: presZoomScale,
afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
focusAndFinish(forceDidFocus || didFocus);
@@ -252,75 +303,60 @@ export class DocumentManager {
} 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)));
+ createViewFunc(Doc.BrushDoc(targetDoc), () => focusAndFinish(true)); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
} 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..
wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
- targetDocContext._viewTransition = 'transform 500ms';
+
+ if (targetDocContext.layoutKey === 'layout_icon') {
+ return targetDocContextView.iconify(
+ () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)),
+ 30
+ );
+ }
+
+ const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500;
+ const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined;
+ targetDocContextView.setViewTransition('transform', contextFocusTime);
+ // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially
+ this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished));
targetDocContextView.props.focus(targetDocContextView.rootDoc, {
- willZoom,
+ ...options,
+ zoomTime: contextFocusTime,
+ // originalTarget, // needed?
afterFocus: async () => {
- targetDocContext._viewTransition = undefined;
- if (targetDocContext.layoutKey === 'layout_icon') {
- targetDocContextView.iconify(() =>
- this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
- );
- }
- return ViewAdjustment.doNothing;
- },
- });
-
- // 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;
- targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
- finished?.();
- } else {
- // no timecode means we need to find the context view and focus on our target
- const findView = (delay: number) => {
- const retryDocView = getFirstDocView(resolvedTarget); // 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.
- Doc.linkFollowHighlight(retryDocView.rootDoc);
- retryDocView.focus(targetDoc, {
- willZoom,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- !noSelect && focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- }); // focus on the target in the context
- } else if (delay > 1000) {
- // 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) {
+ // 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;
+ targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
+ finished?.();
+ } else {
+ // otherwise, just look for the target document in this context view now that we've focused the context view
+ if (this.getFirstDocumentView(resolvedTarget)) {
+ // test again for the target view snce we presumably created the context above by focusing on it
+ this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished);
+ } else if (targetDoc.layout) {
// there will no layout for a TEXTANCHOR type document
createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target
}
- } else {
- setTimeout(() => findView(delay + 200), 200);
}
- };
- setTimeout(() => findView(0), 0);
- }
+ return ViewAdjustment.doNothing;
+ },
+ });
} else {
- if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
- const docContextView = this.getFirstDocumentView(docContext[0]);
- if (docContextView) {
- return docContextView.iconify(() =>
- this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
- );
- }
+ if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') {
+ Doc.deiconifyView(docContextPath[0]);
+ this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished);
+ } else {
+ // there's no context view so we need to create one first and try again when that finishes
+ createViewFunc(
+ targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+ () => this.jumpToDocument(targetDoc, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished)
+ );
}
- // there's no context view so we need to create one first and try again when that finishes
- 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
- );
}
}
}
@@ -330,12 +366,12 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc);
const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView);
if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) {
- dv.props.focus(dv.props.Document, { willZoom: true });
+ dv.props.focus(dv.props.Document, { willPanZoom: true });
Doc.linkFollowHighlight(dv?.props.Document, false);
} else {
const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
const showDoc = context || doc;
- CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+ CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), OpenWhereMod.right) && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
}
}
ScriptingGlobals.add(DocFocusOrOpen);