diff options
author | bobzel <zzzman@gmail.com> | 2022-12-01 10:13:03 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2022-12-01 10:13:03 -0500 |
commit | 66184a172006de4d4bf72d9da33858e04d298181 (patch) | |
tree | 9c9fc08d92102410383f7780b4e249616301f8a8 | |
parent | 9d88adb19c2caf715b56c5ed40a500b9ef1491aa (diff) |
refactored process of following links / jumping to docs and added following options for zoomTime, etc instead of setting temporary fields on docs.
27 files changed, 354 insertions, 353 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index eed839520..d13d96dd3 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -157,7 +157,7 @@ export class DocumentOptions { _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom] _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed - _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target + _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target _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 @@ -272,7 +272,7 @@ export class DocumentOptions { clipWidth?: number; // percent transition from before to after in comparisonBox dockingConfig?: string; annotationOn?: Doc; - isPushpin?: boolean; + followLinkToggle?: boolean; isGroup?: boolean; // whether a collection should use a grouping UI behavior _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document noteType?: string; @@ -1703,7 +1703,7 @@ export namespace DocUtils { } export function LeavePushpin(doc: Doc, annotationField: string) { - if (doc.isPushpin) return undefined; + if (doc.followLinkToggle) return undefined; const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { @@ -1711,7 +1711,7 @@ export namespace DocUtils { title: 'pushpin', label: '', annotationOn: Cast(doc.annotationOn, Doc, null), - isPushpin: true, + followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 4f02a8202..1b63b615b 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -5,7 +5,7 @@ import { Cast, DocCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { LightboxView } from '../views/LightboxView'; -import { DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; @@ -171,19 +171,13 @@ export class DocumentManager { }; 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 + finished?: () => void ): void => { - originalTarget = originalTarget ?? targetDoc; - const docView = this.getFirstDocumentView(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; @@ -195,14 +189,14 @@ export class DocumentManager { } const focusAndFinish = (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); + !options.noSelect && docView?.select(false); } finished?.(); }; @@ -216,9 +210,8 @@ export class DocumentManager { if (annoContainerView.props.Document.layoutKey === 'layout_icon') { annoContainerView.iconify(() => annoContainerView.focus(targetDoc, { + ...options, originalTarget, - willZoom, - scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { focusAndFinish(true); @@ -232,13 +225,12 @@ export class DocumentManager { } } if (focusView) { - !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox - if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc); + !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox + 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); @@ -262,13 +254,12 @@ export class DocumentManager { targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden targetDocContext._viewTransition = 'transform 500ms'; targetDocContextView.props.focus(targetDocContextView.rootDoc, { - willZoom, + ...options, + // 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) - ); + targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished)); } return ViewAdjustment.doNothing; }, @@ -281,56 +272,35 @@ export class DocumentManager { finished?.(); } else { // no timecode means we need to find the context view and focus on our target - const findView = (delay: number) => { - const retryDocView = this.getFirstDocumentView(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) { - // 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); + const retryDocView = this.getFirstDocumentView(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, { + ...options, + // originalTarget -- needed? + afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + !options.noSelect && focusAndFinish(true); + res(ViewAdjustment.doNothing); + }), + }); // focus on the target in the context + } 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 { 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) - ); + return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished)); } } // 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, - willZoom, - (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), - docContext, - linkDoc, - true /* if target not found, get rid of context just created */, - originatingDoc, - finished, - originalTarget - ) + () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished) ); } } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 37571ae01..916eee4b7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,27 +1,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { BatchedArray } from "array-batcher"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import Measure, { ContentRect } from "react-measure"; -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast } from "../../../fields/Types"; -import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes"; -import { Utils } from "../../../Utils"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; -import { DocumentManager } from "../DocumentManager"; -import "./DirectoryImportBox.scss"; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; -import React = require("react"); +import { BatchedArray } from 'array-batcher'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import Measure, { ContentRect } from 'react-measure'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { BoolCast, Cast, NumCast } from '../../../fields/Types'; +import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes'; +import { Utils } from '../../../Utils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; +import { DocumentManager } from '../DocumentManager'; +import './DirectoryImportBox.scss'; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry'; +import React = require('react'); -const unsupported = ["text/html", "text/plain"]; +const unsupported = ['text/html', 'text/plain']; @observer export class DirectoryImportBox extends React.Component<FieldViewProps> { @@ -29,7 +29,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { @observable private top = 0; @observable private left = 0; private dimensions = 50; - @observable private phase = ""; + @observable private phase = ''; private disposer: Opt<IReactionDisposer>; @observable private entries: ImportMetadataEntry[] = []; @@ -40,7 +40,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { @observable private uploading = false; @observable private removeHover = false; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(DirectoryImportBox, fieldKey); + } constructor(props: FieldViewProps) { super(props); @@ -71,7 +73,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { runInAction(() => { this.uploading = true; - this.phase = "Initializing download..."; + this.phase = 'Initializing download...'; }); const docs: Doc[] = []; @@ -79,7 +81,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const files = e.target.files; if (!files || files.length === 0) return; - const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0]; + const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0]; const validated: File[] = []; for (let i = 0; i < files.length; i++) { @@ -100,7 +102,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const sizes: number[] = []; const modifiedDates: number[] = []; - runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); + runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`)); const batched = BatchedArray.from(validated, { batchSize: 15 }); const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => { @@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { modifiedDates.push(file.lastModified); }); collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch))); - runInAction(() => this.completed += batch.length); + runInAction(() => (this.completed += batch.length)); }); - await Promise.all(uploads.map(async response => { - const { source: { type }, result } = response; - if (result instanceof Error) { - return; - } - const { accessPaths, exifData } = result; - const path = Utils.prepend(accessPaths.agnostic.client); - const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 }); - const { data, error } = exifData; - if (document) { - Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); - docs.push(document); - } - })); + await Promise.all( + uploads.map(async response => { + const { + source: { type }, + result, + } = response; + if (result instanceof Error) { + return; + } + const { accessPaths, exifData } = result; + const path = Utils.prepend(accessPaths.agnostic.client); + const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 })); + const { data, error } = exifData; + if (document) { + Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); + docs.push(document); + } + }) + ); for (let i = 0; i < docs.length; i++) { const doc = docs[i]; @@ -146,7 +153,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { _height: 500, _chromeHidden: true, x: NumCast(doc.x), - y: NumCast(doc.y) + offset + y: NumCast(doc.y) + offset, }; const parent = this.props.ContainingCollectionView; if (parent) { @@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { if (docs.length < 50) { importContainer = Docs.Create.MasonryDocument(docs, options); } else { - const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")]; + const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')]; importContainer = Docs.Create.SchemaDocument(headers, docs, options); } - runInAction(() => this.phase = 'External: uploading files to Google Photos...'); + runInAction(() => (this.phase = 'External: uploading files to Google Photos...')); await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); - Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); + Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); - DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []); + DocumentManager.Instance.jumpToDocument(importContainer, { willZoom: true }, undefined, []); } runInAction(() => { @@ -169,14 +176,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { this.quota = 1; this.completed = 0; }); - } + }; componentDidMount() { - this.selector.current!.setAttribute("directory", ""); - this.selector.current!.setAttribute("webkitdirectory", ""); + this.selector.current!.setAttribute('directory', ''); + this.selector.current!.setAttribute('webkitdirectory', ''); this.disposer = reaction( () => this.completed, - completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`) + completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)) ); } @@ -193,7 +200,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const offset = this.dimensions / 2; this.left = bounds.width / 2 - offset; this.top = bounds.height / 2 - offset; - } + }; @action addMetadataEntry = async () => { @@ -201,8 +208,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { entryDoc.checked = false; entryDoc.key = keyPlaceholder; entryDoc.value = valuePlaceholder; - Doc.AddDocToList(this.props.Document, "data", entryDoc); - } + Doc.AddDocToList(this.props.Document, 'data', entryDoc); + }; @action remove = async (entry: ImportMetadataEntry) => { @@ -217,7 +224,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { } } } - } + }; render() { const dimensions = 50; @@ -228,193 +235,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const uploading = this.uploading; const showRemoveLabel = this.removeHover; const persistent = this.persistent; - let percent = `${completed / quota * 100}`; - percent = percent.split(".")[0]; - percent = percent.startsWith("100") ? "99" : percent; + let percent = `${(completed / quota) * 100}`; + percent = percent.split('.')[0]; + percent = percent.startsWith('100') ? '99' : percent; const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; - const message = <span className={"phase"}>{this.phase}</span>; - const centerPiece = this.phase.includes("Google Photos") ? - <img src={"/assets/google_photos.png"} style={{ - transition: "0.4s opacity ease", - width: 30, - height: 30, - opacity: uploading ? 1 : 0, - pointerEvents: "none", - position: "absolute", - left: 12, - top: this.top + 10, - fontSize: 18, - color: "white", - marginLeft: this.left + marginOffset - }} /> - : <div + const message = <span className={'phase'}>{this.phase}</span>; + const centerPiece = this.phase.includes('Google Photos') ? ( + <img + src={'/assets/google_photos.png'} style={{ - transition: "0.4s opacity ease", + transition: '0.4s opacity ease', + width: 30, + height: 30, opacity: uploading ? 1 : 0, - pointerEvents: "none", - position: "absolute", + pointerEvents: 'none', + position: 'absolute', + left: 12, + top: this.top + 10, + fontSize: 18, + color: 'white', + marginLeft: this.left + marginOffset, + }} + /> + ) : ( + <div + style={{ + transition: '0.4s opacity ease', + opacity: uploading ? 1 : 0, + pointerEvents: 'none', + position: 'absolute', left: 10, top: this.top + 12.3, fontSize: 18, - color: "white", - marginLeft: this.left + marginOffset - }}>{percent}%</div>; + color: 'white', + marginLeft: this.left + marginOffset, + }}> + {percent}% + </div> + ); return ( <Measure offset onResize={this.preserveCentering}> - {({ measureRef }) => - <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} > + {({ measureRef }) => ( + <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}> {message} <input - id={"selector"} + id={'selector'} ref={this.selector} onChange={this.handleSelection} type="file" style={{ - position: "absolute", - display: "none" - }} /> + position: 'absolute', + display: 'none', + }} + /> <label - htmlFor={"selector"} + htmlFor={'selector'} style={{ opacity: isEditing ? 0 : 1, - pointerEvents: isEditing ? "none" : "all", - transition: "0.4s ease opacity" - }} - > - <div style={{ - width: dimensions, - height: dimensions, - borderRadius: "50%", - background: "black", - position: "absolute", - left: this.left, - top: this.top - }} /> - <div style={{ - position: "absolute", - left: this.left + 8, - top: this.top + 10, - opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + pointerEvents: isEditing ? 'none' : 'all', + transition: '0.4s ease opacity', }}> - <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} /> + <div + style={{ + width: dimensions, + height: dimensions, + borderRadius: '50%', + background: 'black', + position: 'absolute', + left: this.left, + top: this.top, + }} + /> + <div + style={{ + position: 'absolute', + left: this.left + 8, + top: this.top + 10, + opacity: uploading ? 0 : 1, + transition: '0.4s opacity ease', + }}> + <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} /> </div> <img style={{ width: 80, height: 80, - transition: "0.4s opacity ease", + transition: '0.4s opacity ease', opacity: uploading ? 0.7 : 0, - position: "absolute", + position: 'absolute', top: this.top - 15, - left: this.left - 15 + left: this.left - 15, }} - src={"/assets/loading.gif"}></img> + src={'/assets/loading.gif'}></img> </label> <input - type={"checkbox"} - onChange={e => runInAction(() => this.persistent = e.target.checked)} + type={'checkbox'} + onChange={e => runInAction(() => (this.persistent = e.target.checked))} style={{ margin: 0, - position: "absolute", + position: 'absolute', left: 10, bottom: 10, opacity: isEditing || uploading ? 0 : 1, - transition: "0.4s opacity ease", - pointerEvents: isEditing || uploading ? "none" : "all" + transition: '0.4s opacity ease', + pointerEvents: isEditing || uploading ? 'none' : 'all', }} checked={this.persistent} - onPointerEnter={action(() => this.removeHover = true)} - onPointerLeave={action(() => this.removeHover = false)} + onPointerEnter={action(() => (this.removeHover = true))} + onPointerLeave={action(() => (this.removeHover = false))} /> <p style={{ - position: "absolute", + position: 'absolute', left: 27, bottom: 8.4, fontSize: 12, opacity: showRemoveLabel ? 1 : 0, - transition: "0.4s opacity ease" - }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p> + transition: '0.4s opacity ease', + }}> + Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload + </p> {centerPiece} <div style={{ - position: "absolute", + position: 'absolute', top: 10, right: 10, - borderRadius: "50%", + borderRadius: '50%', width: 25, height: 25, - background: "black", - pointerEvents: uploading ? "none" : "all", + background: 'black', + pointerEvents: uploading ? 'none' : 'all', opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + transition: '0.4s opacity ease', }} - title={isEditing ? "Back to Upload" : "Add Metadata"} - onClick={action(() => this.editingMetadata = !this.editingMetadata)} + title={isEditing ? 'Back to Upload' : 'Add Metadata'} + onClick={action(() => (this.editingMetadata = !this.editingMetadata))} /> <FontAwesomeIcon style={{ - pointerEvents: "none", - position: "absolute", + pointerEvents: 'none', + position: 'absolute', right: isEditing ? 14 : 15, top: isEditing ? 15.4 : 16, opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + transition: '0.4s opacity ease', }} - icon={isEditing ? "cloud-upload-alt" : "tag"} + icon={isEditing ? 'cloud-upload-alt' : 'tag'} color="#FFFFFF" - size={"1x"} + size={'1x'} /> <div style={{ - transition: "0.4s ease opacity", - width: "100%", - height: "100%", - pointerEvents: isEditing ? "all" : "none", + transition: '0.4s ease opacity', + width: '100%', + height: '100%', + pointerEvents: isEditing ? 'all' : 'none', opacity: isEditing ? 1 : 0, - overflowY: "scroll" - }} - > + overflowY: 'scroll', + }}> <div style={{ - borderRadius: "50%", + borderRadius: '50%', width: 25, height: 25, marginLeft: 10, - position: "absolute", + position: 'absolute', right: 41, - top: 10 + top: 10, }} - title={"Add Metadata Entry"} - onClick={this.addMetadataEntry} - > + title={'Add Metadata Entry'} + onClick={this.addMetadataEntry}> <FontAwesomeIcon style={{ - pointerEvents: "none", + pointerEvents: 'none', marginLeft: 6.4, - marginTop: 5.2 + marginTop: 5.2, }} - icon={"plus"} - size={"1x"} + icon={'plus'} + size={'1x'} /> </div> - <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p> - <hr style={{ margin: "6px 10px 12px 10px" }} /> - {entries.map(doc => + <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p> + <hr style={{ margin: '6px 10px 12px 10px' }} /> + {entries.map(doc => ( <ImportMetadataEntry Document={doc} key={doc[Id]} remove={this.remove} - ref={(el) => { if (el) this.entries.push(el); }} + ref={el => { + if (el) this.entries.push(el); + }} next={this.addMetadataEntry} /> - )} + ))} </div> </div> - } + )} </Measure> ); } - -}
\ No newline at end of file +} diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 282116f1b..4f742817a 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -1,10 +1,11 @@ import { action, runInAction } from 'mobx'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentDecorations } from '../views/DocumentDecorations'; import { LightboxView } from '../views/LightboxView'; -import { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView'; +import { PresEffect, PresEffectDirection } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; import { LinkManager } from './LinkManager'; import { UndoManager } from './UndoManager'; @@ -56,7 +57,7 @@ export class LinkFollower { createTabForTarget(false); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); + docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget }); } }; runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value @@ -81,52 +82,45 @@ export class LinkFollower { const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; - const followLinks = sourceDoc.isPushpin || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); + const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); var count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); followLinks.forEach(async linkDoc => { - if (linkDoc) { - 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) { - const zoom = BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false); - if (target.TourMap) { - const fieldKey = Doc.LayoutFieldKey(target); - const tour = DocListCast(target[fieldKey]).reverse(); - LightboxView.SetLightboxDoc(currentContext, undefined, tour); - setTimeout(LightboxView.Next); - allFinished(); - } else { - const containerAnnoDoc = Cast(target.annotationOn, Doc, null); - const containerDoc = containerAnnoDoc || target; - var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]); - while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { - containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; - } - const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; - DocumentManager.Instance.jumpToDocument( - target, - zoom, - (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), - targetContexts, - linkDoc, - undefined, - sourceDoc, - allFinished, - undefined, - undefined, - Cast(sourceDoc.presZoom, 'number', null) - ); - } - } else { + 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) { + const options: DocFocusOptions = { + playAudio: BoolCast(sourceDoc.followLinkAudio), + toggleTarget: BoolCast(sourceDoc.followLinkToggle), + willZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false), + zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500), + zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null), + effect: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffect) as PresEffect, + effectDirection: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffectDirection) as PresEffectDirection, + originatingDoc: sourceDoc, + }; + if (target.TourMap) { + const fieldKey = Doc.LayoutFieldKey(target); + const tour = DocListCast(target[fieldKey]).reverse(); + LightboxView.SetLightboxDoc(currentContext, undefined, tour); + setTimeout(LightboxView.Next); allFinished(); + } else { + const containerAnnoDoc = Cast(target.annotationOn, Doc, null); + const containerDoc = containerAnnoDoc || target; + var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]); + while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { + containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + } + const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; + DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, allFinished); } } else { allFinished(); diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 4b0310e76..4cd45fc9c 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -392,7 +392,7 @@ export class SharingManager extends React.Component<{}> { onClick={() => { let context: Opt<CollectionView>; if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) { - DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]); + DocumentManager.Instance.jumpToDocument(this.targetDoc, { willZoom: true }, undefined, [context.props.Document]); } }} onPointerEnter={action(() => { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 043a83d16..c9c09b63b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -147,7 +147,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin - Doc.SetInPlace(doc, 'isPushpin', undefined, true); + Doc.SetInPlace(doc, 'followLinkToggle', undefined, true); doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true); }) ); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index aa2818e0d..36ea3ef18 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -41,7 +41,7 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles'; import './InkStroke.scss'; import { InkStrokeProperties } from './InkStrokeProperties'; import { InkTangentHandles } from './InkTangentHandles'; -import { DocComponentView } from './nodes/DocumentView'; +import { DocComponentView, DocFocusOptions } from './nodes/DocumentView'; import { FieldView, FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { StyleProp } from './StyleProvider'; @@ -83,8 +83,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { return this._subContentView?.getAnchor?.() || this.rootDoc; }; - scrollFocus = (textAnchor: Doc, smooth: boolean) => { - return this._subContentView?.scrollFocus?.(textAnchor, smooth); + scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => { + return this._subContentView?.scrollFocus?.(textAnchor, options); }; /** * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 165bb69bb..965cd560f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -206,7 +206,7 @@ export class MainView extends React.Component { document.addEventListener('dash', (e: any) => { // event used by chrome plugin to tell Dash which document to focus on const id = FormattedTextBox.GetDocFromUrl(e.detail); - DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, false, undefined, []) : null)); + DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willZoom: false }, undefined, []) : null)); }); document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener); this.initEventListeners(); diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 1162cde50..07371c9d5 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -92,7 +92,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 3e199919e..f7cc32cff 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1412,14 +1412,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow)); @undoBatch - changeAnimationBehavior = action((behavior: string) => this.destinationAnchor && (this.destinationAnchor.presEffect = behavior); + changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior)); @undoBatch - changeEffectDirection = action((effect: PresEffectDirection) => this.destinationAnchor && (this.destinationAnchor.presEffectDirection = effect); + changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect)); animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const lanch = this.destinationAnchor; - const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.MEDIUM_BLUE : ''; + const lanch = this.sourceAnchor; + const color = lanch?.linkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.linkAnimDirection) ? Colors.MEDIUM_BLUE : ''; return ( <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div @@ -1514,7 +1514,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; - this.sourceAnchor && (this.sourceAnchor.presZoom = scale); + this.sourceAnchor && (this.sourceAnchor.linkZoomScale = scale); }; /** @@ -1530,7 +1530,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { render() { const isNovice = Doc.noviceMode; - const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3)); + const zoom = Number((NumCast(this.sourceAnchor?.linkZoomScale, 1) * 100).toPrecision(3)); const targZoom = this.sourceAnchor?.followLinkZoom; const indent = 30; const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)); @@ -1621,7 +1621,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> <p>Animation</p> - <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.destinationAnchor?.presEffect, 'default')}> + <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.linkAnimEffect, 'default')}> <option value="default">Default</option> {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( <option value={effect.toString()}>{effect.toString()}</option> @@ -1639,9 +1639,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { '0.1', '0.1', '10', - NumCast(this.destinationAnchor?.presTransition) / 1000, + NumCast(this.sourceAnchor?.linkTransitionTime) / 1000, true, - (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.destinationAnchor && (this.destinationAnchor.presTransition = timeInMS)), + (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.linkTransitionTime = timeInMS)), indent )}{' '} <div @@ -1668,6 +1668,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> </button> </div> + <div className="propertiesView-input inline"> + <p>Toggle Target (Show/Hide)</p> + <button + style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> <p>Zoom %</p> <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 29670a1a7..5e389e17e 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -193,13 +193,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } } const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing); this.props.focus(this.rootDoc, { - willZoom: options?.willZoom, - scale: options?.scale, + ...options, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), }); }; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ac6391365..08aebc62d 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -255,13 +255,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } } const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing; this.props.focus(this.rootDoc, { - willZoom: options?.willZoom, - scale: options?.scale, + ...options, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), }); }; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 7696777fd..13984171c 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -297,7 +297,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); CollectionDockingView.AddSplit(curPres, OpenWhereMod.right); - setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things + setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willZoom: false }, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things } setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } @@ -376,7 +376,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { focusFunc = (doc: Doc, options: DocFocusOptions) => { const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; if (options?.willZoom !== false && shrinkwrap && this._document) { - const focusSpeed = NumCast(this._document.focusSpeed, 500); + const focusSpeed = options.zoomTime ?? 500; shrinkwrap(); this._document._viewTransition = `transform ${focusSpeed}ms`; setTimeout( diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 56a5c3dcc..5fb3c1ac6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1164,7 +1164,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined }; const newState = HistoryUtil.getState(); const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc; - const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined); + const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.zoomScale || 0.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, panY }; @@ -1172,7 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } // focus on the document in the collection const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); - const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0; + const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 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) { scale && (this.Document[this.scaleFieldKey] = scale); @@ -1538,8 +1538,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - scrollFocus = (anchor: Doc, smooth: boolean) => { - const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition); + scrollFocus = (anchor: Doc, options: DocFocusOptions) => { + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; return PresBox.restoreTargetDocView( this.rootDoc, // { pinDocLayout: BoolCast(anchor.presPinDocLayout) }, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3fd6ca803..9df3e195f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -541,7 +541,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.y = NumCast(d.y) - this.Bounds.top; return d; }); - const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' }); + const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' }); const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' }); DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', ''); diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 9778fc4fe..924e07daa 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -150,7 +150,7 @@ export class CollectionLinearView extends CollectionSubView() { <span className="bottomPopup-text"> Currently playing: {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => ( - <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true, undefined, [])}> + <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willZoom: true }, undefined, [])}> {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')} </span> ))} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index a22999f52..c0dfeedfa 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -214,7 +214,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); // Jump to the this document - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + DocumentManager.Instance.jumpToDocument(this._rowDoc, { willZoom: false }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc)); } }; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 48e32c071..fb20887cb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -53,6 +53,7 @@ import { RadialMenu } from './RadialMenu'; import { ScriptingBox } from './ScriptingBox'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); +import { PresEffect, PresEffectDirection } from './trails'; const { Howl } = require('howler'); interface Window { @@ -95,10 +96,17 @@ export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that 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 + zoomScale?: number; // percent of containing frame to zoom into document + zoomTime?: number; afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) + effect?: PresEffect; // animation effect for focus + effectDirection?: PresEffectDirection; + noSelect?: boolean; // whether target should be selected after focusing + playAudio?: boolean; // whether to play audio annotation on focus + toggleTarget?: boolean; // whether to toggle target on and off + originatingDoc?: Doc; // document that triggered the focus } export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>; export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void; @@ -106,7 +114,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p export interface DocComponentView { updateIcon?: () => void; // updates the icon representation of the document getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) - scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus + scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. @@ -566,7 +574,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]))); // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant !== true && !LinkDocPreview.LinkInfo); + const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false }); const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing; this.props.focus(options?.docTransform ? anchor : this.rootDoc, { ...options, @@ -741,8 +749,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => { this.Document.ignoreClick = false; if (setPushpin) { - this.Document.isPushpin = !this.Document.isPushpin; - this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton; + this.Document.followLinkToggle = !this.Document.followLinkToggle; + this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton; } else { this.Document._isLinkButton = !this.Document._isLinkButton; } @@ -759,14 +767,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = true; + this.Document.followLinkToggle = true; }; @undoBatch @action followLinkOnClick = (location: Opt<string>, zoom: boolean): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; }; @@ -775,7 +783,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps selectOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = false; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.onClick = this.layoutDoc.onClick = undefined; }; @undoBatch @@ -934,7 +942,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' }); !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' }); onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' }); - onClicks.push({ description: (this.Document.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' }); + onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' }); onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (DocListCast(this.Document.links).length) { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 1d79febdf..416107859 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import React = require('react'); import { PresBox } from './trails'; -import { DocumentViewProps } from './DocumentView'; +import { DocFocusOptions, DocumentViewProps } from './DocumentView'; export const pageSchema = createSchema({ googlePhotosUrl: 'string', @@ -62,8 +62,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; @action - scrollFocus = (anchor: Doc, smooth: boolean) => { - const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition); + scrollFocus = (anchor: Doc, options: DocFocusOptions) => { + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; return PresBox.restoreTargetDocView( this.rootDoc, // { pinDocLayout: BoolCast(anchor.presPinDocLayout) }, @@ -168,7 +168,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 39e323247..b19c4a9e2 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -28,6 +28,7 @@ import './PDFBox.scss'; import { VideoBox } from './VideoBox'; import React = require('react'); import { PresBox } from './trails'; +import { DocFocusOptions } from './DocumentView'; @observer export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { @@ -94,7 +95,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; @@ -200,16 +201,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps brushView = (view: { width: number; height: number; panX: number; panY: number }) => { this._pdfViewer?.brushView(view); }; - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, options: DocFocusOptions) => { let didToggle = false; if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(!options.instant); didToggle = true; } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; this._initialScrollTarget = doc; - PresBox.restoreTargetDocView(this.rootDoc, {}, doc, NumCast(doc.presTransition, NumCast(doc.focusSpeed, 500)), { pannable: doc.presPinData ? true : false }); - return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), smooth) ?? (didToggle ? 1 : undefined); + PresBox.restoreTargetDocView(this.rootDoc, {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false }); + return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined); }; getAnchor = () => { const docAnchor = () => { @@ -277,7 +278,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; if (this._initialScrollTarget) { - this.scrollFocus(this._initialScrollTarget, false); + this.scrollFocus(this._initialScrollTarget, { instant: true }); this._initialScrollTarget = undefined; } }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 82d5b00f9..607cd6187 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import './VideoBox.scss'; import { ObjectField } from '../../../fields/ObjectField'; -import { OpenWhere } from './DocumentView'; +import { DocFocusOptions, OpenWhere } from './DocumentView'; const path = require('path'); /** @@ -950,7 +950,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); }; - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, options: DocFocusOptions) => { if (doc !== this.rootDoc) { const showTime = Cast(doc._timecodeToShow, 'number', null); showTime !== undefined && setTimeout(() => this.Seek(showTime), 100); @@ -1006,7 +1006,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Doc.GetProto(region).backgroundColor = 'transparent'; Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001; this.addDocument(region); const anchx = NumCast(cropping.x); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 5ce6a0eb1..64b186489 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu'; import { Annotation } from '../pdf/Annotation'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocumentViewProps } from './DocumentView'; +import { DocFocusOptions, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { VideoBox } from './VideoBox'; @@ -284,17 +284,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); - scrollFocus = (doc: Doc, smooth: boolean) => { - if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth); + scrollFocus = (doc: Doc, options: DocFocusOptions) => { + if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant); if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(options.instant); } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; if (doc !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); if (scrollTo !== undefined && this._initialScroll === undefined) { - const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0; + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; this.goTo(scrollTo, focusSpeed); return focusSpeed; } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ce4639b76..9e91f6c46 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { DocumentViewInternal, OpenWhere } from '../DocumentView'; +import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -689,9 +689,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {}); @undoBatch - makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin); + makePushpin = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); - isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin); + isPushpin = (anchor: Doc) => BoolCast(anchor.followLinkToggle); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; @@ -922,10 +922,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return anchorDoc ?? this.rootDoc; } - scrollFocus = (textAnchor: Doc, smooth: boolean) => { + scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => { let didToggle = false; if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(options.instant); didToggle = true; } const textAnchorId = textAnchor[Id]; @@ -966,7 +966,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const content = (ret.frag as any)?.content; if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { - smooth && (this._focusSpeed = 500); + !options.instant && (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 diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index c04b79a1e..4073677f3 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -15,7 +15,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty import { AudioField } from '../../../../fields/URLField'; import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; -import { Docs } from '../../../documents/Documents'; +import { Docs, DocumentOptions } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -35,7 +35,7 @@ import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; import { map } from 'bluebird'; -import { OpenWhere, OpenWhereMod } from '../DocumentView'; +import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView'; const { Howl } = require('howler'); export interface PinProps { @@ -313,6 +313,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.rootDoc._itemIndex = index; const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + let focusSpeed = 500; if (activeItem.presActiveFrame !== undefined) { const transTime = NumCast(activeItem.presTransition, 500); const context = DocCast(DocCast(activeItem.presentationTargetDoc).context); @@ -334,11 +335,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') { this.startTempMedia(targetDoc, activeItem); } - if (targetDoc) { - // Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc); - targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500))); - setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10); - } if (targetDoc?.lastFrame !== undefined) { targetDoc._currentFrame = 0; } @@ -583,8 +579,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc); } else if (targetDoc && activeItem.presMovement !== PresMovement.None) { LightboxView.SetLightboxDoc(undefined); - const zooming = activeItem.presMovement !== PresMovement.Pan; - DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, activeItem, finished, undefined, true, NumCast(activeItem.presZoom, 1)); + const options: DocFocusOptions = { + willZoom: activeItem.presMovement !== PresMovement.Pan, + zoomScale: NumCast(activeItem.presZoom, 1), + zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500), + noSelect: true, + originatingDoc: activeItem, + }; + DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished); } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); } @@ -1199,7 +1201,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _batch: UndoManager.Batch | undefined = undefined; - public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?:number) => { + public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { let batch: any; return ( <input @@ -1208,7 +1210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { min={min} max={max} value={value} - style={{marginLeft: hmargin, marginRight:hmargin, width: `calc(100% - ${2*(hmargin??0)}px)`}} + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { batch = UndoManager.StartBatch('pres slider'); diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 133b882f6..7069ff399 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -55,9 +55,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { pinToPres = () => this.props.pinToPres(this.annoTextRegion, {}); @undoBatch - makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin); + makePushpin = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle); - isPushpin = () => BoolCast(this.annoTextRegion.isPushpin); + isPushpin = () => BoolCast(this.annoTextRegion.followLinkToggle); @action onPointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5a5c63c3d..0703ca9b4 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; -import { DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import { StyleProp } from '../StyleProvider'; @@ -166,7 +166,7 @@ export class PDFViewer extends React.Component<IViewerProps> { // 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, scrollTop: number, smooth: boolean) => { + scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { @@ -174,7 +174,7 @@ export class PDFViewer extends React.Component<IViewerProps> { const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight)); if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { if (!this._pdfViewer) this._initialScroll = scrollTo; - else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo); + else if (!options.instant) smoothScroll((focusSpeed = options.zoomTime??500), mainCont, scrollTo); else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } } else { @@ -295,7 +295,7 @@ export class PDFViewer extends React.Component<IViewerProps> { @action scrollToAnnotation = (scrollToAnnotation: Doc) => { if (scrollToAnnotation) { - this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), true); + this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), {zoomTime: 500}); Doc.linkFollowHighlight(scrollToAnnotation); } }; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index c5177de90..9c656e093 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -407,7 +407,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * or opening it in a new tab. */ selectElement = async (doc: Doc, finishFunc: () => void) => { - await DocumentManager.Instance.jumpToDocument(doc, true, undefined, [], undefined, undefined, undefined, finishFunc); + await DocumentManager.Instance.jumpToDocument(doc, { willZoom: true }, undefined, [], finishFunc); }; /** |