diff options
Diffstat (limited to 'src/client/util')
-rw-r--r-- | src/client/util/DocumentManager.ts | 98 | ||||
-rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 334 | ||||
-rw-r--r-- | src/client/util/LinkFollower.ts | 84 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 2 |
4 files changed, 250 insertions, 268 deletions
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(() => { |