From 205fdacf9807be2691e13e1d9d04d6b98620391a Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 15 Jan 2025 13:03:59 -0500 Subject: fixed so that users are sentt to dropbox to authorized uploads once, and then a refresh token is used afterwards. fixed up some collection views to work better with the tagsView. fixed grid view to show entire image. tweaked notetaking view to work better with tags, but it's incomplete. --- src/client/views/collections/CollectionNoteTakingView.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx') diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index ac8e37358..323a679cc 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils'; -import { Doc, Field, FieldType, Opt } from '../../../fields/Doc'; +import { Doc, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -48,6 +48,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @computed get notetakingCategoryField() { return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column')); } + toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset')); public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; @@ -136,7 +137,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const rowCol = this.docsDraggedRowCol; // this will sort the docs into the correct columns (minus the ones you're currently dragging) docs.forEach(d => { - const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; + const sectionValue = this.toHeader(d); // look for if header exists already const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); if (existingHeader) { @@ -161,7 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { }; @computed get allFieldValues() { - return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField]))); + return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField])))); } componentDidMount() { @@ -313,7 +314,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // how to get the width of a document. Currently returns the width of the column (minus margins) // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) getDocWidth = (d: Doc) => { - const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType); + const heading = this.toHeader(d); const existingHeader = this.colHeaderData.find(sh => sh.heading === heading); const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0; const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; @@ -427,7 +428,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); this.childDocs?.forEach(d => { if (d instanceof Promise) return; - const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; + const sectionValue = this.toHeader(d); if (sectionValue.toString() === colHeader) { docsMatchingHeader.push(d); } -- cgit v1.2.3-70-g09d2 From 132fb0c203f426c508eeb23dd33b508e0608d19f Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 10 Feb 2025 09:38:32 -0500 Subject: code cleanup in createDocumentTool. added childLayoutFitWidth to comparisonBox so flashcards can fill window. --- src/ClientUtils.ts | 9 ++++++ .../views/collections/CollectionNoteTakingView.tsx | 2 +- src/client/views/nodes/ComparisonBox.tsx | 6 ++-- src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 2 -- .../views/nodes/chatbot/agentsystem/Agent.ts | 2 +- .../nodes/chatbot/tools/CreateDocumentTool.ts | 25 ++++----------- src/client/views/nodes/trails/PresBox.tsx | 1 - src/server/ApiManagers/AssistantManager.ts | 37 ++++++++++++---------- 8 files changed, 43 insertions(+), 41 deletions(-) (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx') diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index e5dbb81db..8c9dd0a32 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -365,6 +365,15 @@ export namespace ClientUtils { } } +/** + * Removes specified keys from an object and returns the result in the 'omit' field of the return value. + * The keys that were removed ared retuned in the 'extract' field of the return value. + * @param obj - object to remove keys from + * @param keys - list of key field names to remove + * @param pattern - optional pattern to specify keys to removed + * @param addKeyFunc - optional function to call with object after keys have been removed + * @returns a tuple object containint 'omit' (oject after keys have been removed) and 'extact' (object containing omitted fields) + */ export function OmitKeys(obj: object, keys: string[], pattern?: string, addKeyFunc?: (dup: object) => void): { omit: { [key: string]: unknown }; extract: { [key: string]: unknown } } { const omit: { [key: string]: unknown } = { ...obj }; const extract: { [key: string]: unknown } = {}; diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 323a679cc..4c23d4c48 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils'; -import { Doc, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 53fbd11c5..cb0831d3c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -10,7 +10,7 @@ import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { Animation, DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; -import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; import { nullAudio } from '../../../fields/URLField'; import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; @@ -677,6 +677,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); + childFitWidth = () => Cast(this.Document.childLayoutFitWidth, 'boolean') ?? Cast(this.Document.childLayoutFitWidth, 'boolean'); + displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); @@ -696,7 +698,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() isDocumentActive={returnFalse} isContentActive={this.childActiveFunc} showTags={undefined} - fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option + fitWidth={this.childFitWidth} // set to returnTrue to make images fill the comparisonBox-- should be a user option ignoreUsePath={layoutString ? true : undefined} moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index c69cd8e89..eb0431b85 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -41,7 +41,6 @@ // }; // _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; -// childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; // addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); // removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); // render() { @@ -69,7 +68,6 @@ // chromeHidden={true} // childHideResizeHandles={true} // childHideDecorationTitle={true} -// childLayoutFitWidth={this.childLayoutFitWidth} // // childDocumentsActive={returnFalse} // removeDocument={this.removeDoc} // addDocument={this.addDoc} diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 25e471ce8..13fdac7f5 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -54,7 +54,7 @@ export class Agent { history: () => string, csvData: () => { filename: string; id: string; text: string }[], addLinkedUrlDoc: (url: string, id: string) => void, - addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions, id: string) => void, + addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions, id?: string) => void, // eslint-disable-next-line @typescript-eslint/no-unused-vars createCSVInDash: (url: string, title: string, id: string, data: string) => void ) { diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts index b919b242c..07b515fea 100644 --- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts +++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts @@ -1,8 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { BaseTool } from './BaseTool'; import { Observation } from '../types/types'; import { ParametersType } from '../types/tool_types'; import { DocumentOptions } from '../../../../documents/Documents'; +import { OmitKeys } from '../../../../../ClientUtils'; export enum supportedDocumentTypes { flashcard = 'flashcard', @@ -360,9 +360,9 @@ type CreateListDocToolParamsType = typeof createListDocToolParams; // Tool class for creating documents export class CreateDocTool extends BaseTool { - private _addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions, id: string) => void; + private _addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions) => void; - constructor(addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions, id: string) => void) { + constructor(addLinkedDoc: (doc_type: supportedDocumentTypes, data: unknown, options: DocumentOptions) => void) { super( 'createDoc', `Creates one or more documents that best fit the user’s request. @@ -395,24 +395,13 @@ export class CreateDocTool extends BaseTool { async execute(args: ParametersType): Promise { try { const parsedDoc = JSON.parse(args.docs) as ({ doc_type: supportedDocumentTypes; data: unknown } & DocumentOptions)[]; - parsedDoc.forEach(doc => + parsedDoc.forEach( + doc => this._addLinkedDoc( doc.doc_type, doc.data, - { - title: doc.title, - backgroundColor: doc.backgroundColor, - text_fontColor: doc.text_fontColor, - _width: doc._width, - _height: doc._height, - type_collection: doc.type_collection, - _layout_fitWidth: false, - _layout_autoHeight: true, - x: doc.x, - y: doc.y, - }, - uuidv4() - ) + {...OmitKeys(doc, ["data", "doc_type"]).omit, _layout_fitWidth: false, _layout_autoHeight: true} + ) // prettier-ignore ); return [{ type: 'text', text: 'Created document.' }]; } catch (error) { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 7c23ca8e1..3ce4dc6cb 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -2989,7 +2989,6 @@ export class PresBox extends ViewBoxBaseComponent() { ignoreUnrendered childDragAction={dropActionType.move} setContentViewBox={emptyFunction} - // childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts index b53581aa2..86af756a4 100644 --- a/src/server/ApiManagers/AssistantManager.ts +++ b/src/server/ApiManagers/AssistantManager.ts @@ -9,7 +9,7 @@ */ import { Readability } from '@mozilla/readability'; -import axios, { AxiosResponse } from 'axios'; +import axios from 'axios'; import { spawn } from 'child_process'; import * as fs from 'fs'; import { writeFile } from 'fs'; @@ -122,8 +122,6 @@ export default class AssistantManager extends ApiManager { const { query, max_results } = req.body; const MIN_VALID_RESULTS_RATIO = 0.75; // 3/4 threshold let startIndex = 1; // Start at the first result initially - let validResults: any[] = []; - const fetchSearchResults = async (start: number) => { return customsearch.cse.list({ q: query, @@ -135,20 +133,27 @@ export default class AssistantManager extends ApiManager { }); }; - const filterResultsByXFrameOptions = async (results: any[]) => { + const filterResultsByXFrameOptions = async ( + results: { + url: string | null | undefined; + snippet: string | null | undefined; + }[] + ) => { const filteredResults = await Promise.all( - results.map(async result => { - try { - const urlResponse: AxiosResponse = await axios.head(result.url, { timeout: 5000 }); - const xFrameOptions = urlResponse.headers['x-frame-options']; - if (xFrameOptions && xFrameOptions.toUpperCase() === 'SAMEORIGIN') { - return result; + results + .filter(result => result.url) + .map(async result => { + try { + const urlResponse = await axios.head(result.url!, { timeout: 5000 }); + const xFrameOptions = urlResponse.headers['x-frame-options']; + if (xFrameOptions && xFrameOptions.toUpperCase() === 'SAMEORIGIN') { + return result; + } + } catch (error) { + console.error(`Error checking x-frame-options for URL: ${result.url}`, error); } - } catch (error) { - console.error(`Error checking x-frame-options for URL: ${result.url}`, error); - } - return null; // Exclude the result if it doesn't match - }) + return null; // Exclude the result if it doesn't match + }) ); return filteredResults.filter(result => result !== null); // Remove null results }; @@ -163,7 +168,7 @@ export default class AssistantManager extends ApiManager { })) || []; // Filter the initial results - validResults = await filterResultsByXFrameOptions(initialResults); + let validResults = await filterResultsByXFrameOptions(initialResults); // If valid results are less than 3/4 of max_results, fetch more results while (validResults.length < max_results * MIN_VALID_RESULTS_RATIO) { -- cgit v1.2.3-70-g09d2 From 1ab6f6c87b746e0c2898694216db50d5faadf7f5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 6 Mar 2025 19:46:43 -0500 Subject: a bunch of changes to improve how docs are selected automatically when created. --- src/client/documents/DocUtils.ts | 5 +- src/client/views/GlobalKeyHandler.ts | 19 +++-- src/client/views/MarqueeAnnotator.tsx | 2 +- src/client/views/SidebarAnnos.tsx | 9 +-- .../collections/CollectionMasonryViewFieldRow.tsx | 7 +- .../views/collections/CollectionNoteTakingView.tsx | 2 +- .../collections/CollectionNoteTakingViewColumn.tsx | 21 ++---- .../views/collections/CollectionStackingView.tsx | 2 +- .../CollectionStackingViewFieldColumn.tsx | 26 ++----- .../views/collections/CollectionTreeView.tsx | 2 +- src/client/views/collections/TreeView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 5 +- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- .../collectionGrid/CollectionGridView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 6 ++ src/client/views/nodes/EquationBox.tsx | 10 +-- src/client/views/nodes/LabelBox.tsx | 5 +- src/client/views/nodes/MapBox/MapBox.tsx | 2 +- src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 2 +- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 2 +- .../views/nodes/formattedText/DailyJournal.tsx | 25 +++---- .../views/nodes/formattedText/FormattedTextBox.tsx | 41 +++------- src/fields/Doc.ts | 4 - src/fields/RichTextField.ts | 87 +++++++++++++++++----- 24 files changed, 149 insertions(+), 141 deletions(-) (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 7e0416447..9797a407b 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -385,7 +385,7 @@ export namespace DocUtils { newDoc.x = x; newDoc.y = y; newDoc[DocData].backgroundColor = Doc.UserDoc().textBackgroundColor; - Doc.SetSelectOnLoad(newDoc); + DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } @@ -408,6 +408,7 @@ export namespace DocUtils { description: ':Daily Journal', event: undoable(() => { const newDoc = Docs.Create.DailyJournalDocument('', { x, y }); + DocumentView.SetSelectOnLoad(newDoc); docAdder?.(newDoc); }, 'Create Daily Journal'), icon: 'book', @@ -456,7 +457,7 @@ export namespace DocUtils { newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; - Doc.SetSelectOnLoad(newDoc); + DocumentView.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index b200aff65..2d342d1b1 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -23,7 +23,6 @@ import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocume import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhereMod } from './nodes/OpenWhere'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { AnchorMenu } from './pdf/AnchorMenu'; const modifiers = ['control', 'meta', 'shift', 'alt']; @@ -75,7 +74,6 @@ export class KeyManager { public handle = action((e: KeyboardEvent) => { // accumulate buffer of characters to insert in a new text note. once the note is created, it will stop keyboard events from reaching this function. - if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar += e.key === 'Enter' ? '\n' : e.key; const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(/* keyname */); @@ -106,6 +104,10 @@ export class KeyManager { }; private unmodified = action((keyname: string, e: KeyboardEvent) => { + const nothing = { + stopPropagation: false, + preventDefault: false, + }; switch (keyname) { case 'u': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { @@ -169,17 +171,14 @@ export class KeyManager { return { stopPropagation: true, preventDefault: true }; } break; - case 'arrowleft': return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself. - case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right'); - case 'arrowup': return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up'); - case 'arrowdown': return (e.target as any).type !== 'text' && this.nudge(0, 1, 'nudge down'); + case 'arrowleft': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(-1, 0, 'nudge left') : nothing; // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself. + case 'arrowright': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge( 1, 0, 'nudge right') : nothing; + case 'arrowup': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(0, -1, 'nudge up') : nothing; + case 'arrowdown': return (e.target as HTMLInputElement | null)?.type !== 'text'? this.nudge(0, 1, 'nudge down'): nothing; default: } // prettier-ignore - return { - stopPropagation: false, - preventDefault: false, - }; + return nothing; }); private shift = action((keyname: string) => { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 02516264c..3f4200dce 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -199,7 +199,7 @@ export class MarqueeAnnotator extends ObservableReactComponent { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 87076bf65..816fc8ed3 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -3,23 +3,23 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnAll, returnFalse, returnOne, returnZero } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Field, FieldType, FieldResult, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { DocCast, NumCast, StrCast } from '../../fields/Types'; +import { DocUtils } from '../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; -import { DocUtils } from '../documents/DocUtils'; import { SearchUtil } from '../util/SearchUtil'; import { Transform } from '../util/Transform'; import { ObservableReactComponent } from './ObservableReactComponent'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProp'; import { CollectionStackingView } from './collections/CollectionStackingView'; +import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; interface ExtraProps { fieldKey: string; @@ -81,8 +81,7 @@ export class SidebarAnnos extends ObservableReactComponent this.addNewTextDoc('-typed text-', false, true), 'add text note'); - // addNewTextDoc is called when a user starts typing in a column to create a new node - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; + addTextNote = undoable(() => { const key = this._props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); + const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true }); const colValue = this.getValue(this._props.heading); newDoc[key] = colValue; - Doc.SetSelectOnLoad(newDoc); - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + DocumentView.SetSelectOnLoad(newDoc); return this._props.addDocument?.(newDoc) || false; - }; + }, 'add text note'); // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. // If the user deletes the first column, the documents get moved to the second column. Otherwise, @@ -177,7 +172,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent { const key = this._props.pivotField; doc[key] = this.getValue(this._props.heading); - Doc.SetSelectOnLoad(doc); + DocumentView.SetSelectOnLoad(doc); return this._props.addDocument?.(doc); }, this._props.addDocument, @@ -249,7 +244,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent
- +
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 96125f2c2..72e6bb8f2 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -321,7 +321,7 @@ export class CollectionStackingView extends CollectionSubView { SnappingManager.IsDragging && (this._background = '#b4b4b4'); } // prettier-ignore @action pointerLeave = () => { this._background = 'inherit'}; // prettier-ignore - @undoBatch typedNote = () => this.addNewTextDoc('-typed text-', false, true); - - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; + @undoBatch typedNote = () => { const key = this._props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); + const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true }); key && (newDoc[key] = this.getValue(this._props.heading)); const maxHeading = this._props.docList.reduce((prevHeading, doc) => (NumCast(doc.heading) > prevHeading ? NumCast(doc.heading) : prevHeading), 0); const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; - Doc.SetSelectOnLoad(newDoc); - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + DocumentView.SetSelectOnLoad(newDoc); return this._props.addDocument?.(newDoc) || false; }; @@ -240,7 +235,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const height = this._ele ? DivHeight(this._ele) : 0; DocUtils.addDocumentCreatorMenuItems( doc => { - Doc.SetSelectOnLoad(doc); + DocumentView.SetSelectOnLoad(doc); return this._props.addDocument?.(doc); }, this._props.addDocument, @@ -394,14 +389,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< onKeyDown={e => e.stopPropagation()} className="collectionStackingView-addDocumentButton" style={{ width: 'calc(100% - 25px)', maxWidth: this._props.columnWidth / this._props.numGroupColumns - 25, marginBottom: 10 }}> - } - menuCallback={this.menuCallback} - /> + } menuCallback={this.menuCallback} />
) : null} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a60cd98ac..e93724dd4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -174,7 +174,7 @@ export class CollectionTreeView extends CollectionSubView 0 && prev) { - Doc.SetSelectOnLoad(prev); + DocumentView.SetSelectOnLoad(prev); DocumentView.getDocumentView(prev, this.DocumentView?.())?.select(false); } return true; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index ab4d8b060..6208905fe 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1300,7 +1300,7 @@ export class TreeView extends ObservableReactComponent { const fieldKey = Doc.LayoutFieldKey(newParent); if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) { remove(child); - Doc.SetSelectOnLoad(child); + DocumentView.SetSelectOnLoad(child); TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined; Doc.AddDocToList(newParent, fieldKey, child, addAfter, false); newParent.treeView_Open = true; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 43addfc29..89aa53c35 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -293,7 +293,7 @@ export class CollectionFreeFormView extends CollectionSubView this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this._props.isAnyChildContentActive(); addLiveTextBox = (newDoc: Doc) => { - Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newDoc); }; selectDocuments = (docs: Doc[]) => { @@ -1496,8 +1496,7 @@ export class CollectionFreeFormView extends CollectionSubView { const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 }); - Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + DocumentView.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed Doc.AddDocToList(this.Document, this._props.fieldKey, text); this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY)))); }), diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 595abc7f8..a514ee04e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1110,6 +1110,12 @@ export class DocumentView extends DocComponent() { public static DeselectAll: (except?: Doc) => void | undefined; public static DeselectView: (dv: DocumentView | undefined) => void | undefined; public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined; + + public static SelectOnLoad: Doc | undefined; + public static SetSelectOnLoad(doc?: Doc) { + DocumentView.SelectOnLoad = doc; + doc && DocumentView.addViewRenderedCb(doc, dv => dv.select(false)); + } /** * returns a list of all currently selected DocumentViews */ diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 576b5bbe0..dcc6e27ed 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -30,12 +30,12 @@ export class EquationBox extends ViewBoxBaseComponent() { componentDidMount() { this._props.setContentViewBox?.(this); - if (Doc.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) { + if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) { this._props.select(false); - this._ref.current!.mathField.focus(); - this.dataDoc.text === 'x' && this._ref.current!.mathField.select(); - Doc.SetSelectOnLoad(undefined); + this._ref.current?.mathField.focus(); + this.dataDoc.text === 'x' && this._ref.current?.mathField.select(); + DocumentView.SetSelectOnLoad(undefined); } reaction( () => this._props.isSelected(), @@ -66,7 +66,7 @@ export class EquationBox extends ViewBoxBaseComponent() { color: StrCast(this.Document.color), fontSize: this.fontSize, }); - Doc.SetSelectOnLoad(nextEq); + DocumentView.SetSelectOnLoad(nextEq); this._props.addDocument?.(nextEq); e.stopPropagation(); } diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index dcf9e1fed..7fb83571f 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import './LabelBox.scss'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { RichTextMenu } from './formattedText/RichTextMenu'; +import { DocumentView } from './DocumentView'; @observer export class LabelBox extends ViewBoxBaseComponent() { @@ -230,8 +231,8 @@ export class LabelBox extends ViewBoxBaseComponent() { if (this._divRef) { this._divRef.addEventListener('beforeinput', this.beforeInput); - if (Doc.SelectOnLoad === this.Document) { - Doc.SelectOnLoad = undefined; + if (DocumentView.SelectOnLoad === this.Document) { + DocumentView.SetSelectOnLoad(undefined); this._divRef.focus(); } this.fitTextToBox(this._divRef); diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 541b41bf7..792cb6b46 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -353,7 +353,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; const docView = this.DocumentView?.(); diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index eb0431b85..a27a8bda1 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -32,7 +32,7 @@ // addNoteClick = (e: React.PointerEvent) => { // setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { // const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); -// Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed +// DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed // Doc.AddDocToList(this.props.place, 'data', newDoc); // this._stack?.scrollToBottom(); // e.stopPropagation(); diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 95f89a573..0627d382e 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -237,7 +237,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; const docView = this.DocumentView?.(); diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index 9decbfaf0..7999357b0 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -51,22 +51,13 @@ export class DailyJournal extends ViewBoxAnnotatableComponent() @action setDailyText() { console.log('setDailyText() called...'); - const initialText = `Journal Entry - ${this.journalDate}\n\nStart writing here...`; + const placeholderText = 'Start writing here...'; + const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`; console.log('Checking if dataDoc has text field...'); console.log('Setting new text field with:', initialText); - this.dataDoc[this.fieldKey] = new RichTextField( - JSON.stringify({ - doc: { - type: 'doc', - content: [{ type: 'paragraph', content: [{ type: 'text', text: initialText }] }], - }, - selection: { type: 'text', anchor: 1, head: 1 }, - storedMarks: [], - }), - initialText - ); + this.dataDoc[this.fieldKey] = RichTextField.textToRtf(initialText, undefined, placeholderText.length); console.log('Current text field:', this.dataDoc[this.fieldKey]); } @@ -85,10 +76,11 @@ export class DailyJournal extends ViewBoxAnnotatableComponent() } render() { - return
- - -
; + return ( +
+ +
+ ); } } @@ -99,6 +91,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, { _height: 35, _xMargin: 10, _yMargin: 10, + _layout_autoHeight: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c2a2caecf..83ee619d0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -99,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { [key: string]: NodeViewConstructor }) { FormattedTextBox._nodeViews = nodeViews; } // prettier-ignore public static PasteOnLoad: ClipboardEvent | undefined; - public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection @@ -286,7 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); - Doc.SetSelectOnLoad(target); + DocumentView.SetSelectOnLoad(target); return target; }; @@ -1542,44 +1541,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; + let { tr } = this.EditorView.state; if (selLoadChar) { - const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; - const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks); - const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); - const tr = tr2.setStoredMarks(storedMarks); - - this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - } else if (!FormattedTextBox.DontSelectInitialText) { - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - selectAll(this.EditorView.state, (tx: Transaction) => { - this.EditorView?.dispatch(tx.addStoredMark(mark)); - }); - this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1)))); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - } else { - const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); - const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; - const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - const { tr } = this.EditorView.state; - this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); - this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data + tr = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); } + this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } if (selectOnLoad) { - FormattedTextBox.DontSelectInitialText = false; this.EditorView!.focus(); } if (this._props.isContentActive()) this.prepareForTyping(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index bdd41b0bb..805bb4526 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -468,10 +468,6 @@ export class Doc extends RefField { } } export namespace Doc { - export let SelectOnLoad: Doc | undefined; - export function SetSelectOnLoad(doc: Doc | undefined) { - SelectOnLoad = doc; - } export let DocDragDataName: string = ''; export function SetDocDragDataName(name: string) { DocDragDataName = name; diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index dc636031a..4b1403b88 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -48,26 +48,77 @@ export class RichTextField extends ObjectField { '' ); } + static Initialize = (initial?: string) => { + const content: object[] = []; + const state = { + doc: { + type: 'doc', + content, + }, + selection: { + type: 'text', + anchor: 0, + head: 0, + }, + }; + if (initial && initial.length) { + content.push({ + type: 'paragraph', + content: { + type: 'text', + text: initial, + }, + }); + state.selection.anchor = state.selection.head = initial.length + 1; + } + return JSON.stringify(state); + }; + + private static ToProsemirrorState = (plainText: string, selectBack?: number, delimeter = '\n') => { + // Remap the text, creating blocks split on newlines + const elements = plainText.split(delimeter); + + // Google Docs adds in an extra carriage return automatically, so this counteracts it + !elements[elements.length - 1].length && elements.pop(); + + // Preserve the current state, but re-write the content to be the blocks + const parsed: Record = JSON.parse(RichTextField.Initialize()); + (parsed.doc as Record).content = elements.map(text => { + const paragraph: object = { + type: 'paragraph', + content: text.length ? [{ type: 'text', marks: [], text }] : undefined, // An empty paragraph gets treated as a line break + }; + return paragraph; + }); - public static textToRtf(text: string, imgDocId?: string) { + // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it + parsed.selection = { type: 'text', anchor: 2 + plainText.length - (selectBack ?? 0), head: 2 + plainText.length }; + + // Export the ProseMirror-compatible state object we've just built + return JSON.stringify(parsed); + }; + + public static textToRtf(text: string, imgDocId?: string, selectBack?: number) { return new RichTextField( - JSON.stringify({ - // this is a RichText json that has the question text placed above a related image - doc: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, - content: [ - ...(text ? [{ type: 'text', text }] : []), // - ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []), - ], - }, - ], - }, - selection: { type: 'text', anchor: 2, head: 2 }, - }), + !imgDocId + ? this.ToProsemirrorState(text, selectBack) + : JSON.stringify({ + // this is a RichText json that has the question text placed above a related image + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + ...(text ? [{ type: 'text', text }] : []), // + ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []), + ], + }, + ], + }, + selection: { type: 'text', anchor: 2 + text.length, head: 2 + text.length }, + }), text ); } -- cgit v1.2.3-70-g09d2