diff options
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 212 |
1 files changed, 136 insertions, 76 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 29266bd8e..fc2da18d9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -10,6 +10,7 @@ import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fi import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { PrefetchProxy } from '../../../fields/Proxy'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -19,10 +20,11 @@ import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocOptions, DocUtils, Docs } from '../../documents/Documents'; +import { DocUtils, Docs } from '../../documents/Documents'; import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; +import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -36,9 +38,10 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent, ViewBoxInterface } from '../DocComponent'; import { EditableView } from '../EditableView'; +import { FieldsDropdown } from '../FieldsDropdown'; import { GestureOverlay } from '../GestureOverlay'; import { LightboxView } from '../LightboxView'; -import { StyleProp } from '../StyleProvider'; +import { AudioAnnoState, StyleProp } from '../StyleProvider'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; @@ -47,7 +50,6 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; -import { FieldsDropdown } from '../FieldsDropdown'; interface Window { MediaRecorder: MediaRecorder; } @@ -102,12 +104,11 @@ export interface DocumentViewProps extends FieldViewSharedProps { childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. dragWhenActive?: boolean; dontHideOnDrag?: boolean; - suppressSetHeight?: boolean; onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected DataTransition?: () => string | undefined; NativeWidth?: () => number; NativeHeight?: () => number; - contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; @@ -199,6 +200,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document ((!this.disableClickScriptFunc && // this.onClickHandler && !this._props.onBrowseClickScript?.() && + !this.layoutDoc.layout_isSvg && this.isContentActive() !== true) || this.isContentActive() === false) ? 'none' @@ -221,7 +223,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document)) ); } - @computed get _allLinks() { + @computed get _allLinks(): Doc[] { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); } @@ -457,11 +459,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); setToggleDetail = undoable( - (defaultLayout: string, scriptFieldKey: 'onClick') => + (scriptFieldKey: 'onClick') => (this.Document[scriptFieldKey] = ScriptField.MakeScript( `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) .replace('layout_', '') - .replace(/^layout$/, 'detail')}", "${defaultLayout}")`, + .replace(/^layout$/, 'detail')}")`, { documentView: 'any' } )), 'set toggle detail' @@ -558,7 +560,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); - this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp })); + this._props + .contextMenuItems?.() + .forEach( + item => + item.label && + cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp }) + ); if (!this.Document.isFolder) { const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null); @@ -595,7 +603,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.Document.annotationOn) { onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); @@ -808,8 +816,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; /** * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by - * setting layout_showTitle using the format: field1[;field2[...][:hover]] - * from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] + * setting layout_showTitle using the format: field1[:hover] **/ @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; @@ -836,7 +843,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> - {!dropdownWidth ? null : <div style={{ width: dropdownWidth }}>{this.fieldsDropdown(showTitle)}</div>} + {!dropdownWidth ? null : ( + <div className="documntViewInternal-dropdown" style={{ width: dropdownWidth }}> + {this.fieldsDropdown(showTitle)} + </div> + )} <div style={{ width: `calc(100% - ${dropdownWidth}px)`, @@ -849,21 +860,26 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document contents={ showTitle .split(';') - .map(field => Field.toString(targetDoc[field.trim()] as Field)) + .map(field => Field.toJavascriptString(this.Document[field] as Field)) .join(' \\ ') || '-unset-' } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} - GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))} + GetValue={() => + showTitle + .split(';') + .map(field => Field.toKeyValueString(this.Document, field)) + .join('\\') + } SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { + if (input?.startsWith('$')) { if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = input?.substring(1); + this.layoutDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; + Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title'; } - } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { + } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') { KeyValueBox.SetField(targetDoc, showTitle, input); } return true; @@ -890,6 +906,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document fieldKey={this.layout_showCaption} styleProvider={this.captionStyleProvider} dontRegisterView={true} + rootSelected={this.rootSelected} noSidebar={true} dontScale={true} renderDepth={this._props.renderDepth} @@ -907,7 +924,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : this.docContents ?? ( <div className="documentView-node" - id={this.Document[Id]} + id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined} style={{ ...style, background: this.backgroundBoxColor, @@ -1004,49 +1021,41 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { let gumStream: any; let recorder: any; - navigator.mediaDevices - .getUserMedia({ - audio: true, - }) - .then(function (stream) { - let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); - if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); - DictationManager.Controls.listen({ - interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), - continuous: { indefinite: false }, - }).then(results => { - if (results && [DictationManager.Controls.Infringed].includes(results)) { - DictationManager.Controls.stop(); - } - onEnd?.(); - }); - - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); - if (!(result instanceof Error)) { - const audioField = new AudioField(result.accessPaths.agnostic.client); - const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); - if (audioAnnos === undefined) { - dataDoc[field + '_audioAnnotations'] = new List([audioField]); - } else { - audioAnnos.push(audioField); - } - } - }; - //runInAction(() => (dataDoc.audioAnnoState = 'recording')); - recorder.start(); - const stopFunc = () => { - recorder.stop(); - DictationManager.Controls.stop(false); - runInAction(() => (dataDoc.audioAnnoState = 'stopped')); - gumStream.getAudioTracks()[0].stop(); - }; - if (onRecording) onRecording(stopFunc); - else setTimeout(stopFunc, 5000); + navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { + let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); + if (audioTextAnnos) audioTextAnnos.push(''); + else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); + DictationManager.Controls.listen({ + interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + onEnd?.(); }); + + gumStream = stream; + recorder = new MediaRecorder(stream); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); + if (!(result instanceof Error)) { + const audioField = new AudioField(result.accessPaths.agnostic.client); + const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); + if (audioAnnos) audioAnnos.push(audioField); + else dataDoc[field + '_audioAnnotations'] = new List([audioField]); + } + }; + recorder.start(); + const stopFunc = () => { + recorder.stop(); + DictationManager.Controls.stop(false); + dataDoc.audioAnnoState = AudioAnnoState.stopped; + gumStream.getAudioTracks()[0].stop(); + }; + if (onRecording) onRecording(stopFunc); + else setTimeout(stopFunc, 5000); + }); } } @@ -1059,8 +1068,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; private _animEffectTimer: NodeJS.Timeout | undefined; - public Guid = Utils.GenerateGuid(); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. - + /** + * This is used to create an id for tracking a Doc. Since the Doc can be in a regular view and in the lightbox at + * the same time, this creates a different version of the id depending on whether the search scope will be in the lightbox or not. + * @param inLightbox is the id scoped to the lightbox + * @param id the id + * @returns + */ + public static UniquifyId(inLightbox: boolean | undefined, id: string) { + return (inLightbox ? 'lightbox-' : '') + id; + } + public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. + public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]); @computed public static get exploreMode() { return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); } @@ -1201,7 +1220,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { }; public noOnClick = () => this._docViewInternal?.noOnClick(); public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle); - public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey); + public setToggleDetail = (scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(scriptFieldKey); public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY); public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents(); public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource); @@ -1223,7 +1242,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); + this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } @@ -1231,25 +1250,25 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public playAnnotation = () => { const self = this; - const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; + const audioAnnoState = this.dataDoc.audioAnnoState ?? AudioAnnoState.stopped; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); if (anno instanceof AudioField) { switch (audioAnnoState) { - case 'stopped': + case AudioAnnoState.stopped: this.dataDoc[AudioPlay] = new Howl({ src: [anno.url.href], format: ['mp3'], autoplay: true, loop: false, volume: 0.5, - onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')), + onend: action(() => (self.dataDoc.audioAnnoState = AudioAnnoState.stopped)), }); - this.dataDoc.audioAnnoState = 'playing'; + this.dataDoc.audioAnnoState = AudioAnnoState.playing; break; - case 'playing': + case AudioAnnoState.playing: this.dataDoc[AudioPlay]?.stop(); - this.dataDoc.audioAnnoState = 'stopped'; + this.dataDoc.audioAnnoState = AudioAnnoState.stopped; break; } } @@ -1279,7 +1298,48 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined); }, 'set custom view'); + public static setDefaultTemplate(checkResult?: boolean) { + if (checkResult) { + return Doc.UserDoc().defaultTextLayout; + } + const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined; + undoable(() => { + var tempDoc: Opt<Doc>; + if (view) { + if (!view.layoutDoc.isTemplateDoc) { + tempDoc = view.Document; + MakeTemplate(tempDoc); + Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); + Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc)); + tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); + } else { + tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); + if (!tempDoc) { + tempDoc = view.Document; + while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto); + } + } + } + Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; + }, 'set default template')(); + } + + /** + * This switches between the current view of a Doc and a specified alternate layout view. + * The current view of the Doc is stored in the layout_default field so that it can be restored. + * If the current view of the Doc is already the specified alternate layout view, this will switch + * back to the original layout (stored in layout_default) + * @param detailLayoutKeySuffix the name of the alternate layout field key (NOTE: 'layout_' will be prepended to this string to get the actual field nam) + */ + public toggleDetail = (detailLayoutKeySuffix: string) => { + const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', ''); + if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout; + const defaultLayout = StrCast(this.Document.layout_default); + if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); + else this.switchViews(true, detailLayoutKeySuffix, undefined, true); + }; public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + const batch = UndoManager.StartBatch('switchView:' + view); runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc setTimeout( action(() => { @@ -1292,6 +1352,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { setTimeout( action(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0); + batch.end(); finished?.(); }), Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) @@ -1374,7 +1435,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1451,9 +1512,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); }); -ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') { - if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); - else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); +ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { + dv.toggleDetail(detailLayoutKeySuffix); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { |