From 4548ec81efc434c8ee5d89b896b2088e4e61d7a0 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 28 Feb 2024 22:22:20 -0500 Subject: extracted field dropdown menu to a components and then cleaned up collectionTimeView, Documentview titles, and FilterPanel to share it --- src/client/views/global/globalScripts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 51672513b..e73ab25b5 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -78,7 +78,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole } if (SelectionManager.Views.length) { SelectionManager.Docs.forEach(doc => { - Doc.GetProto(doc).layout_headingColor = color; + doc[DocData].layout_headingColor = color === 'transparent' ? undefined : color; doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title'); }); } else { -- cgit v1.2.3-70-g09d2 From 4cce99ec8d0bb2a45e19dd967dbbb8348c68abe8 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 28 Feb 2024 22:46:23 -0500 Subject: fixed double click of some icons to make them persist (circle, rectangle, fitall). --- src/client/util/CurrentUserUtils.ts | 4 +--- src/client/views/global/globalScripts.ts | 12 ++++++------ src/client/views/nodes/FontIconBox/FontIconBox.tsx | 10 ++++++++-- 3 files changed, 15 insertions(+), 11 deletions(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 289384be0..84bed08fa 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -611,9 +611,7 @@ export class CurrentUserUtils { return [ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "View All", icon: "object-group", toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true, - { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "object-group", toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index e73ab25b5..33704e8fe 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -98,7 +98,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) { const selected = SelectionManager.Docs.lastElement(); // prettier-ignore const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([ @@ -112,16 +112,16 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid }], ['viewAll', { checkResult: (doc:Doc) => BoolCast(doc?._freeform_fitContentsToBox, false), - setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, + setDoc: (doc:Doc,dv:DocumentView) => { + if (persist) doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox; + else if (doc._freeform_fitContentsToBox) doc._freeform_fitContentsToBox = undefined; + else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce(); + }, }], ['center', { checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false), setDoc: (doc:Doc,dv:DocumentView) => doc._stacking_alignCenter = !doc._stacking_alignCenter, }], - ['fitOnce', { - checkResult: (doc:Doc) => false, - setDoc: (doc:Doc, dv:DocumentView) => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce() - }], ['clusters', { waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false), diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 3577cc8d9..91b6de80b 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -314,6 +314,7 @@ export class FontIconBox extends ViewBoxBaseComponent() { const tooltip = StrCast(this.Document.toolTip); const script = ScriptCast(this.Document.onClick); + const double = ScriptCast(this.Document.onDoubleClick); const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); @@ -330,7 +331,12 @@ export class FontIconBox extends ViewBoxBaseComponent() { //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} - onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })} + onPointerDown={e => + setupMoveUpEvents(this, e, returnTrue, emptyFunction, (e, doubleTap) => { + !doubleTap && script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + doubleTap && double.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + }) + } /> ); } -- cgit v1.2.3-70-g09d2 From a9eb266296d1b71f1016c867f39e20299c011eea Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 9 Mar 2024 23:08:15 -0500 Subject: added a template button to top bar to set default layout. fixed show title when value is a list. fixed typeahead for createing notes with templates. --- src/client/util/CurrentUserUtils.ts | 7 ++++--- src/client/views/GlobalKeyHandler.ts | 3 +++ src/client/views/MainView.tsx | 1 + src/client/views/TemplateMenu.tsx | 13 ------------- src/client/views/global/globalScripts.ts | 19 +++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 21 +++++++++++++++------ 7 files changed, 43 insertions(+), 23 deletions(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 564a46514..15255641d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -124,11 +124,11 @@ export class CurrentUserUtils { { title: "Postit", backgroundColor: "yellow", icon: "sticky-note"}, { title: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { title: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; - const reqdNoteList = reqdTempOpts.map(opts => { + const reqdNoteList = [...reqdTempOpts.map(opts => { const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true}; const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.title === opts.title): undefined; return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts)); - }); + }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); @@ -788,7 +788,8 @@ pie title Minerals in my tap water CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, - { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, + { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, + { title: "Template",icon: "scroll", toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF, scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} }, { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 733383002..e800798ca 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -24,6 +24,7 @@ import { MainView } from './MainView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { OpenWhereMod } from './nodes/DocumentView'; import { AnchorMenu } from './pdf/AnchorMenu'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; const modifiers = ['control', 'meta', 'shift', 'alt']; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; @@ -61,6 +62,8 @@ 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 = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key); e.key === 'Control' && (CtrlKey = true); //if (!Doc.noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true); const keyname = e.key && e.key.toLowerCase(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3be52597a..e369e2876 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -397,6 +397,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faPaintBrush, fa.faTimes, fa.faFlag, + fa.faScroll, fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 00195f7d7..eed197b0b 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -45,7 +45,6 @@ class OtherToggle extends React.Component<{ checked: boolean; name: string; togg export interface TemplateMenuProps { docViews: DocumentView[]; - templates?: Map; } @observer @@ -61,17 +60,6 @@ export class TemplateMenu extends React.Component { this.props.docViews.map(dv => dv.switchViews(false, 'layout')); }; - @undoBatch - @action - toggleTemplate = (event: React.ChangeEvent, template: string): void => { - this.props.docViews.forEach(d => (Doc.Layout(d.layoutDoc)['_show' + template] = event.target.checked ? template.toLowerCase() : '')); - }; - - @action - toggleTemplateActivity = (): void => { - this._hidden = !this._hidden; - }; - // todo: add brushes to brushMap to save with a style name onCustomKeypress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -104,7 +92,6 @@ export class TemplateMenu extends React.Component { const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data); const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data); const templateMenu: Array = []; - this.props.templates?.forEach((checked, template) => templateMenu.push()); templateMenu.push(); addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template))); this._addedKeys && diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 33704e8fe..0579b07c7 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -21,6 +21,9 @@ import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { WebBox } from '../nodes/WebBox'; import { VideoBox } from '../nodes/VideoBox'; import { DocData } from '../../../fields/DocSymbols'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { PrefetchProxy } from '../../../fields/Proxy'; +import { MakeTemplate } from '../../util/DropConverter'; ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views.length <= 0; @@ -71,6 +74,22 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b } }); +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) { + if (checkResult) { + return Doc.UserDoc().defaultTextLayout; + } + const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined; + + if (view) { + const tempDoc = view.Document; + if (!view.layoutDoc.isTemplateDoc) { + MakeTemplate(tempDoc); + } + Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc); + tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc); + } else Doc.UserDoc().defaultTextLayout = undefined; +}); // toggle: Set overlay status of selected document ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { if (checkResult) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 431781d24..29266bd8e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -849,7 +849,7 @@ export class DocumentViewInternal extends DocComponent targetDoc[field.trim()]?.toString()) + .map(field => Field.toString(targetDoc[field.trim()] as Field)) .join(' \\ ') || '-unset-' } display="block" diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 46043eefb..f02ad7300 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; @@ -312,6 +312,8 @@ export class FontIconBox extends ViewBoxBaseComponent() { ); } + @observable _hackToRecompute = 0; // bcz: ugh ... 's toggleStatus initializes but doesn't track its value after a click. so a click that does nothing to the toggle state will toggle the button anyway. this forces the Toggle to re-read the ToggleStatus value. + @computed get toggleButton() { // Determine the type of toggle button const buttonText = StrCast(this.dataDoc.buttonText); @@ -319,7 +321,7 @@ export class FontIconBox extends ViewBoxBaseComponent() { const script = ScriptCast(this.Document.onClick); const double = ScriptCast(this.Document.onDoubleClick); - const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; + const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); @@ -336,10 +338,17 @@ export class FontIconBox extends ViewBoxBaseComponent() { icon={this.Icon(color)!} label={this.label} onPointerDown={e => - setupMoveUpEvents(this, e, returnTrue, emptyFunction, (e, doubleTap) => { - (!doubleTap || !double) && script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); - doubleTap && double && double.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); - }) + setupMoveUpEvents( + this, + e, + returnTrue, + emptyFunction, + action((e, doubleTap) => { + (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + this._hackToRecompute = this._hackToRecompute + 1; + }) + ) } /> ); -- cgit v1.2.3-70-g09d2 From a974aa4e6573c8becf93f78610406747fec14c1c Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 19 Mar 2024 17:08:46 -0400 Subject: cleaned up user templates to not get changed on reload. made setting a template add it to the template tools list and as a tools button. fixed linking to parts of a template. fixed disappearing templates caused by stacking view set a field with an empty key. updated field assignment syntax in trees, dash field views, and key value box to all use :,:=,=,=:= syntax. added text elide button. added @(title) syntax for hyperlinking. made using a template both inherit from the template to get default values and use the template to render. fixed submenu placement of context menu. updated RTF markdown doc. --- src/client/util/CurrentUserUtils.ts | 19 +++-- src/client/util/DropConverter.ts | 45 +++++----- src/client/util/LinkManager.ts | 13 +-- src/client/util/RTFMarkup.tsx | 30 +++---- src/client/views/ContextMenuItem.tsx | 2 +- src/client/views/TemplateMenu.scss | 1 + src/client/views/TemplateMenu.tsx | 7 +- .../collections/CollectionMasonryViewFieldRow.tsx | 10 +-- .../CollectionStackingViewFieldColumn.tsx | 6 +- src/client/views/collections/TreeView.tsx | 14 +++- src/client/views/global/globalScripts.ts | 33 +++----- src/client/views/nodes/ComparisonBox.tsx | 71 +++++++++------- src/client/views/nodes/DocumentView.tsx | 32 ++++++- src/client/views/nodes/FieldView.tsx | 1 + src/client/views/nodes/FontIconBox/FontIconBox.tsx | 22 ++--- src/client/views/nodes/KeyValueBox.tsx | 2 +- src/client/views/nodes/KeyValuePair.tsx | 2 +- .../views/nodes/formattedText/DashFieldView.tsx | 57 +++++++++---- .../views/nodes/formattedText/FormattedTextBox.tsx | 34 ++++---- .../views/nodes/formattedText/RichTextMenu.tsx | 13 +++ .../views/nodes/formattedText/RichTextRules.ts | 97 ++++++++++++---------- src/client/views/nodes/formattedText/marks_rts.ts | 5 +- src/client/views/nodes/formattedText/nodes_rts.ts | 2 + src/fields/Doc.ts | 42 +++++----- src/fields/util.ts | 4 + 25 files changed, 328 insertions(+), 236 deletions(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 84a33500d..bbee18707 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -73,7 +73,7 @@ export class CurrentUserUtils { }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; const reqdFuncs = { /* hidden: "IsNoviceMode()" */ }; - return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs); + return DocUtils.AssignScripts(userDocTemplates ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs); } /// Initializes templates for editing click funcs of a document @@ -133,11 +133,19 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } + static setupUserTemplates(doc: Doc, field="template_user") { + const tempUsers = DocCast(doc[field]); + const reqdUserList = DocListCast(tempUsers?.data); + + const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true }; + return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts)); + } /// Initializes collection of templates for notes and click functions static setupDocTemplates(doc: Doc, field="myTemplates") { const templates = [ CurrentUserUtils.setupNoteTemplates(doc), + CurrentUserUtils.setupUserTemplates(doc), CurrentUserUtils.setupClickEditorTemplates(doc) ]; CurrentUserUtils.setupChildClickEditors(doc) @@ -375,9 +383,9 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)}, { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, { toolTip: "Tap or drag to create a mermaid node", title: "Mermaids", icon: "rocket", dragFactory: doc.emptyMermaids as Doc, clickFactory: DocCast(doc.emptyMermaids)}, - { toolTip: "Tap or drag to create a plotly node", title: "Plotly", icon: "rocket", dragFactory: doc.emptyPlotly as Doc, clickFactory: DocCast(doc.emptyMermaids)}, + { toolTip: "Tap or drag to create a plotly node", title: "Plotly", icon: "rocket", dragFactory: doc.emptyPlotly as Doc, clickFactory: DocCast(doc.emptyMermaids)}, { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, - { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "book", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, + { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "book", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)}, @@ -385,10 +393,10 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, - { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, - { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, + { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script @@ -738,6 +746,7 @@ pie title Minerals in my tap water { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, ]}, + { title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}}, { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}}, { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 3df3e36c6..ed5749d06 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -28,6 +28,7 @@ export function MakeTemplate(doc: Doc) { } /** + * * Recursively converts 'doc' into a template that can be used to render other documents. * * For recurive Docs in the template, their target fieldKey is defined by their title, @@ -75,32 +76,36 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { remProps.map(prop => (dbox[prop] = undefined)); } } else if (!doc.onDragStart && !doc.isButtonBar) { - const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; - if (layoutDoc.type !== DocumentType.FONTICON) { - !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); - } - layoutDoc.isTemplateDoc = true; - dbox = Docs.Create.FontIconDocument({ - _nativeWidth: 100, - _nativeHeight: 100, - _width: 100, - _height: 100, - backgroundColor: StrCast(doc.backgroundColor), - title: StrCast(layoutDoc.title), - btnType: ButtonType.ClickButton, - icon: 'bolt', - isSystem: false, - }); - dbox.title = ComputedField.MakeFunction('this.dragFactory.title'); - dbox.dragFactory = layoutDoc; - dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; - dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)'); + dbox = makeUserTemplateButton(doc); } else if (doc.isButtonBar) { dbox.ignoreClick = true; } data.droppedDocuments[i] = dbox; }); } +export function makeUserTemplateButton(doc: Doc) { + const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + if (layoutDoc.type !== DocumentType.FONTICON) { + !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); + } + layoutDoc.isTemplateDoc = true; + const dbox = Docs.Create.FontIconDocument({ + _nativeWidth: 100, + _nativeHeight: 100, + _width: 100, + _height: 100, + backgroundColor: StrCast(doc.backgroundColor), + title: StrCast(layoutDoc.title), + btnType: ButtonType.ClickButton, + icon: 'bolt', + isSystem: false, + }); + dbox.title = ComputedField.MakeFunction('this.dragFactory.title'); + dbox.dragFactory = layoutDoc; + dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); + return dbox; +} ScriptingGlobals.add( function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 0c8d18a7a..cf16c4d6d 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -58,8 +58,8 @@ export class LinkManager { link && action(lAnchProtoProtos => { Doc.AddDocToList(Doc.UserDoc(), 'links', link); - lAnchs[0] && lAnchs[0][DocData][DirectLinks].add(link); - lAnchs[1] && lAnchs[1][DocData][DirectLinks].add(link); + lAnchs[0]?.[DocData][DirectLinks].add(link); + lAnchs[1]?.[DocData][DirectLinks].add(link); }) ) ) @@ -170,10 +170,11 @@ export class LinkManager { console.log('WAITING FOR DOC/PROTO IN LINKMANAGER'); return []; } - const dirLinks = Doc.GetProto(anchor)[DirectLinks]; - const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']); - if (!annos) debugger; - return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice()); + + const dirLinks = Array.from(anchor[DocData][DirectLinks]).filter(l => Doc.GetProto(anchor) === anchor[DocData] || ['1', '2'].includes(LinkManager.anchorIndex(l, anchor) as any)); + const anchorRoot = DocCast(anchor.rootDocument, anchor); // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout) + const annos = DocListCast(anchorRoot[Doc.LayoutFieldKey(anchor) + '_annotations']); + return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks)); }, true); // returns map of group type to anchor's links in that group type diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx index f96d8a5df..315daad42 100644 --- a/src/client/util/RTFMarkup.tsx +++ b/src/client/util/RTFMarkup.tsx @@ -3,18 +3,12 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { MainViewModal } from '../views/MainViewModal'; import { SettingsManager } from './SettingsManager'; -import { Doc } from '../../fields/Doc'; -import { StrCast } from '../../fields/Types'; @observer export class RTFMarkup extends React.Component<{}> { static Instance: RTFMarkup; @observable private isOpen = false; // whether the SharingManager modal is open or not - // private get linkVisible() { - // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; - // } - @action public open = () => (this.isOpen = true); @@ -39,6 +33,10 @@ export class RTFMarkup extends React.Component<{}> { {`wiki:phrase`} {` display wikipedia page for entered text (terminate with carriage return)`}

+

+ {`(( any text ))`} + {` submit text to Chat GPT to have results appended afterward`} +

{`#tag `} {` add hashtag metadata to document. e.g, #idea`} @@ -47,10 +45,6 @@ export class RTFMarkup extends React.Component<{}> { {`#, ## ... ###### `} {` set heading style based on number of '#'s between 1 and 6`}

-

- {`#tag `} - {` add hashtag metadata to document. e.g, #idea`} -

{`>> `} {` add a sidebar text document inline`} @@ -61,7 +55,7 @@ export class RTFMarkup extends React.Component<{}> {

{`cmd-f `} - {` collapse to an inline footnote)`} + {` collapse to an inline footnote`}

{`cmd-e `} @@ -116,20 +110,20 @@ export class RTFMarkup extends React.Component<{}> { {` start a block of text that begins with a hanging indent`}

- {`[:doctitle]] `} + {`@(doctitle) `} {` hyperlink to document specified by it’s title`}

- {`[[fieldname]] `} - {` display value of fieldname`} + {`[@(doctitle.)fieldname] `} + {` display value of fieldname of text document (unless (doctitle.) is used to indicate another document by it's title)`}

- {`[[fieldname=value]] `} - {` assign value to fieldname of document and display it`} + {`[@fieldname:value] `} + {` assign value to fieldname to data document and display it (if '=' is used instead of ':' the value is set on the layout Doc. if value is wrapped in (()) then it will be sent to ChatGPT and the response will replace the value)`}

- {`[[fieldname:doctitle]] `} - {` show value of fieldname from doc specified by it’s title`} + {`[@fieldname:=expression] `} + {` assign a computed expression to fieldname to data document and display it (if '=:=' is used instead of ':=' the expression is set on the layout Doc. if value is wrapped in (()) then it will be sent to ChatGPT and the prompt/response will replace the value)`}

); diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index b2076e1a5..d15ab749c 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -113,7 +113,7 @@ export class ContextMenuItem extends ObservableReactComponent 0 ? '90%' : '20%', + marginLeft: window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%', marginTop, background: SettingsManager.userBackgroundColor, }}> diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss index 4d0f1bf00..36a9ce6d0 100644 --- a/src/client/views/TemplateMenu.scss +++ b/src/client/views/TemplateMenu.scss @@ -39,6 +39,7 @@ display: inline-block; height: 100%; width: 100%; + max-height: 250px; .templateToggle, .chromeToggle { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index eed197b0b..e7269df98 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,7 +1,8 @@ -import { action, computed, observable, ObservableSet, runInAction } from 'mobx'; +import { computed, observable, ObservableSet, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, DocCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; @@ -9,12 +10,10 @@ import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, retu import { Docs, DocUtils } from '../documents/Documents'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { Transform } from '../util/Transform'; -import { undoBatch } from '../util/UndoManager'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView'; import { DefaultStyleProvider } from './StyleProvider'; import './TemplateMenu.scss'; -import { DocData } from '../../fields/DocSymbols'; @observer class TemplateToggle extends React.Component<{ template: string; checked: boolean; toggle: (event: React.ChangeEvent, template: string) => void }> { @@ -99,7 +98,7 @@ export class TemplateMenu extends React.Component { .filter(key => !noteTypes.some(nt => nt.title === key)) .forEach(template => templateMenu.push( this.toggleLayout(e, template)} />)); return ( -
    +
      {Doc.noviceMode ? null : } {templateMenu} Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); + key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !this.onLayoutDoc(key))); } return true; } @@ -128,7 +127,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent Doc.SetInPlace(d, key, castedValue, true)); + key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); this._heading = castedValue.toString(); return true; } @@ -155,10 +154,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent { this._createEmbeddingSelected = false; const key = this._props.pivotField; - this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); + key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); if (this._props.parent.colHeaderData && this._props.headingObject) { const index = this._props.parent.colHeaderData.indexOf(this._props.headingObject); this._props.parent.colHeaderData.splice(index, 1); diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 6a3cb759e..641e01b81 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -112,7 +112,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< if (this._props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } - this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue)); + this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue)); if (this._props.headingObject) { this._props.headingObject.setHeading(castedValue.toString()); this._heading = this._props.headingObject.heading; @@ -137,7 +137,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< if (!value && !forceEmptyNote) return false; const key = this._props.pivotField; const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); - newDoc[key] = this.getValue(this._props.heading); + key && (newDoc[key] = this.getValue(this._props.heading)); const maxHeading = this._props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0); const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; @@ -148,7 +148,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< @action deleteColumn = () => { - this._props.docList.forEach(d => (d[this._props.pivotField] = undefined)); + this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = undefined)); if (this._props.colHeaderData && this._props.headingObject) { const index = this._props.colHeaderData.indexOf(this._props.headingObject); this._props.colHeaderData.splice(index, 1); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 08c00d696..1b4349f44 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -574,11 +574,21 @@ export class TreeView extends ObservableReactComponent {
      value.indexOf(':') !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(':')), value.substring(value.indexOf(':') + 1, value.length), true)} + SetValue={input => { + const match = input.match(/([a-zA-Z0-9_-]+)(=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]+)/); + if (match) { + const key = match[1]; + const assign = match[2]; + const val = match[3]; + KeyValueBox.SetField(doc, key, assign + val, false); + return true; + } + return false; + }} />
      ); diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 0579b07c7..3fdc9a488 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -1,29 +1,26 @@ import { Colors } from 'browndash-components'; import { action, runInAction } from 'mobx'; +import { aggregateBounds } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { aggregateBounds } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; -import { undoable, UndoManager } from '../../util/UndoManager'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; +import { UndoManager, undoable } from '../../util/UndoManager'; import { GestureOverlay } from '../GestureOverlay'; import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; // import { InkTranscription } from '../InkTranscription'; +import { DocData } from '../../../fields/DocSymbols'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../nodes/DocumentView'; -import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; -import { WebBox } from '../nodes/WebBox'; import { VideoBox } from '../nodes/VideoBox'; -import { DocData } from '../../../fields/DocSymbols'; -import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { PrefetchProxy } from '../../../fields/Proxy'; -import { MakeTemplate } from '../../util/DropConverter'; +import { WebBox } from '../nodes/WebBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views.length <= 0; @@ -76,19 +73,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b // toggle: Set overlay status of selected document ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) { - if (checkResult) { - return Doc.UserDoc().defaultTextLayout; - } - const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined; - - if (view) { - const tempDoc = view.Document; - if (!view.layoutDoc.isTemplateDoc) { - MakeTemplate(tempDoc); - } - Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc); - tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc); - } else Doc.UserDoc().defaultTextLayout = undefined; + return DocumentView.setDefaultTemplate(checkResult); }); // toggle: Set overlay status of selected document ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { @@ -197,7 +182,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh map.get(attr)?.setDoc?.(); }); -type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; +type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }]; ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) { @@ -221,6 +206,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: const attrs:attrfuncs[] = [ ['dictation', { checkResult: () => textView?._recordingDictation ? true:false, toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }], + ['elide', { checkResult: () => false, + toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}], ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 715b23fb6..62f630c6c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -159,6 +159,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + + /** + * Tests for whether a comparison box slot (ie, before or after) has renderable text content + * @param whichSlot field key for start or end slot + * @returns a JSX layout string if a text field is found, othwerise undefined + */ + testForTextFields = (whichSlot: string) => { + const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string'; + const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); + const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); + const layoutTemplateString = + slotHasText ? FormattedTextBox.LayoutString(whichSlot): + whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : + altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore + + // A bit hacky to try out the concept of using GPT to fill in flashcards + // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) + // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.). + // eg., this.text_alternate is + // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" + // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field + // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2) + if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) { + var queryText = altText.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... + if (queryText && queryText.match(/\(\(.*\)\)/)) { + KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt + } + } + return layoutTemplateString; + }; + _closeRef = React.createRef(); render() { const clearButton = (which: string) => { @@ -176,48 +207,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() /** * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case * where if there are no Docs in the slots, but the main fieldKey contains text, then - * @param which + * @param whichSlot * @returns */ - const displayDoc = (which: string) => { - const whichDoc = DocCast(this.dataDoc[which]); + const displayDoc = (whichSlot: string) => { + const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); - // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot - // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot - const layoutTemplateString = !targetDoc - ? which.endsWith('1') && subjectText !== undefined - ? FormattedTextBox.LayoutString(this.fieldKey) - : which.endsWith('2') && (this.Document[which] instanceof RichTextField || typeof this.Document[which] === 'string') - ? FormattedTextBox.LayoutString(which) - : undefined - : undefined; - - // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic. - // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) - // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally) - // by the field references in the text (eg., this.text_alternate is - // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" - // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field - // A GPT call will put the "answer" in the second slot of the comparison (eg., text_2) - if (which.endsWith('2') && !layoutTemplateString && !targetDoc) { - var queryText = RTFCast(this.Document[this.fieldKey + '_alternate']) - ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... - .trim(); - if (subjectText && queryText.match(/\(\(.*\)\)/)) { - KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt - } - } + const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot); return targetDoc || layoutTemplateString ? ( <> () hideLinkButton={true} pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> - {layoutTemplateString ? null : clearButton(which)} + {layoutTemplateString ? null : clearButton(whichSlot)} // placeholder image if doc is missing ) : (
      diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9848f18e0..e9ce98583 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,6 +38,7 @@ 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 { AudioAnnoState, StyleProp } from '../StyleProvider'; @@ -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; } @@ -1271,6 +1273,32 @@ export class DocumentView extends DocComponent() { 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; + 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. diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 4ecaaa283..5b47dd91d 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -55,6 +55,7 @@ export interface FieldViewSharedProps { ignoreAutoHeight?: boolean; disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors + ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs) CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f02ad7300..57ae92359 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -5,8 +5,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; -import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -61,23 +60,12 @@ export class FontIconBox extends ViewBoxBaseComponent() { } @observable noTooltip = false; - showTemplate = (): void => { - const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight); - }; - dragAsTemplate = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - }; - useAsPrototype = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); - }; + showTemplate = (dragFactory: Doc) => this._props.addDocTab(dragFactory, OpenWhere.addRight); specificContextMenu = (): void => { - if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) { - const cm = ContextMenu.Instance; - cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' }); + const dragFactory = DocCast(this.layoutDoc.dragFactory); + if (!Doc.noviceMode && dragFactory) { + ContextMenu.Instance.addItem({ description: 'Show Template', event: () => this.showTemplate(dragFactory), icon: 'tag' }); } }; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 2bcad806f..d85432631 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -115,7 +115,7 @@ export class KeyValueBox extends ObservableReactComponent { field === undefined && (field = res.result); } } - if (!key) return field; + if (!key) return false; if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) { target[key] = field; return true; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index d59489a78..f9e8ce4f3 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)} + SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} />
      diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 62cb460c2..6b66d829c 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; @@ -8,12 +8,12 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast } from '../../../../fields/Types'; +import { Cast, DocCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { FilterPanel } from '../../FilterPanel'; @@ -21,6 +21,7 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; +import { DocData } from '../../../../fields/DocSymbols'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -62,6 +63,8 @@ export class DashFieldView { height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} + expanded={node.attrs.expanded} + dataDoc={node.attrs.dataDoc} tbox={tbox} /> ); @@ -89,6 +92,8 @@ interface IDashFieldViewInternal { width: number; height: number; editable: boolean; + expanded: boolean; + dataDoc: boolean; node: any; getPos: any; unclickable: () => boolean; @@ -101,18 +106,19 @@ export class DashFieldViewInternal extends ObservableReactComponent(); @observable _dashDoc: Doc | undefined = undefined; - @observable _expanded = false; + @observable _expanded = this._props.expanded; constructor(props: IDashFieldViewInternal) { super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; - this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + this._textBoxDoc = this._props.tbox.Document; + const setDoc = (doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc); if (this._props.docId) { - DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); } else { - this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + setDoc(this._props.tbox.Document); } } @@ -126,7 +132,9 @@ export class DashFieldViewInternal extends ObservableReactComponent 100; + isRowActive = () => this._expanded && this._props.editable; + finishEdit = action(() => (this._expanded = false)); + selectedCell = (): [Doc, number] => [this._dashDoc!, 0]; // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { @@ -137,18 +145,18 @@ export class DashFieldViewInternal extends ObservableReactComponent this._props.tbox._props.PanelWidth() - 20 : returnZero} - selectedCell={() => [this._dashDoc!, 0]} + maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth} + columnWidth={returnZero} + selectedCell={this.selectedCell} fieldKey={this._fieldKey} rowHeight={returnZero} - isRowActive={() => this._expanded && this._props.editable} + isRowActive={this.isRowActive} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} allowCRs={true} oneLine={!this._expanded} - finishEdit={action(() => (this._expanded = false))} + finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} /> @@ -173,11 +181,21 @@ export class DashFieldViewInternal extends ObservableReactComponent this._dashDoc && (this._dashDoc[this._fieldKey + '_hideKey'] = !this._dashDoc[this._fieldKey + '_hideKey'])), + 'hideKey' + ); + + @computed get _hideKey() { + return this._dashDoc && this._dashDoc[this._fieldKey + '_hideKey']; + } + // clicking on the label creates a pivot view collection of all documents // in the same collection. The pivot field is the fieldKey of this label onPointerDownLabelSpan = (e: any) => { setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { DashFieldViewMenu.createFieldView = this.createPivotForField; + DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide; DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); }); }; @@ -188,6 +206,7 @@ export class DashFieldViewInternal extends ObservableReactComponent ({ value: facet, label: facet })); @@ -203,9 +222,9 @@ export class DashFieldViewInternal extends ObservableReactComponent - {this._props.hideKey ? null : ( + {this._props.hideKey || this._hideKey ? null : ( - {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey} + {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey} )} {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} @@ -224,6 +243,7 @@ export class DashFieldViewInternal extends ObservableReactComponent { static Instance: DashFieldViewMenu; static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + static toggleFieldHide: () => void = emptyFunction; constructor(props: any) { super(props); DashFieldViewMenu.Instance = this; @@ -233,6 +253,10 @@ export class DashFieldViewMenu extends AntimodeMenu { DashFieldViewMenu.createFieldView(e); DashFieldViewMenu.Instance.fadeOut(true); }; + toggleFieldHide = (e: React.MouseEvent) => { + DashFieldViewMenu.toggleFieldHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; @observable _fieldKey = ''; @@ -252,6 +276,9 @@ export class DashFieldViewMenu extends AntimodeMenu { + ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fb709818c..2b48494f2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -451,7 +451,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); + Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } @@ -958,8 +958,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); - optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); + const help = cm.findByDescription('Help...'); + const helpItems = help && 'subitems' in help ? help.subitems : []; + helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: }); + !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; }; @@ -1239,8 +1242,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._editorView?.dispatch(tx.insertText(incomingValue.str))); + } else { + selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); } } } @@ -1339,15 +1342,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ - view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ + view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false, expanded: true }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }], - title: `from: ${DocCast(pdfAnchor.embedContainer).title}`, + title: StrCast(pdfAnchor.title), noPreview: true, - docref: false, + docref: true, + fontSize: '8px', }), - view.state.schema.marks.pFontSize.create({ fontSize: '8px' }), - view.state.schema.marks.em.create({}), ]), ]); @@ -1926,11 +1928,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ); } - cycleAlternateText = () => { - if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; - } + cycleAlternateText = (skipHover?: boolean) => { + this.layoutDoc._layout_enableAltContentUI = true; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined; }; @computed get overlayAlternateIcon() { const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; @@ -1965,7 +1966,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { }); } + elideSelection = () => { + const state = this.view?.state; + if (!state) return; + if (state.selection.empty) return false; + const mark = state.schema.marks.summarize.create(); + const tr = state.tr; + tr.addMark(state.selection.from, state.selection.to, mark); + const content = tr.selection.content(); + const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); + this.view?.dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + return true; + }; + toggleNoAutoLinkAnchor = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index c798ae4b3..b97141e92 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -267,7 +267,7 @@ export class RichTextRules { // toggle alternate text UI %/ new InputRule(new RegExp(/%\//), (state, match, start, end) => { - setTimeout(this.TextBox.cycleAlternateText); + setTimeout(() => this.TextBox.cycleAlternateText(true)); return state.tr.deleteRange(start, end); }), @@ -283,40 +283,26 @@ export class RichTextRules { : tr; }), - new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => { - var count = 0; // ignore first return value which will be the notation that chat is pending a result - KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { - count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string))); - count++; - }); - return null; - }), - - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - // [[ : ]] - // [[:docTitle]] => hyperlink - // [[fieldKey]] => show field - // [[fieldKey{:,=:}=value]] => show field and also set its value - // [[fieldKey:docTitle]] => show field of doc - new InputRule( - new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), - (state, match, start, end) => { - const fieldKey = match[1]; - const assign = match[2] === '=' ? '' : match[2]; - const value = match[4]; - const docTitle = match[5]?.replace(':', ''); + // create a hyperlink to a titled document + // @() + new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@:\.\? \-0-9]+)\)/), (state, match, start, end) => { + const docTitle = match[2]; + const prefixLength = '@('.length; + if (docTitle) { const linkToDoc = (target: Doc) => { - const rstate = this.TextBox.EditorView?.state; - const selection = rstate?.selection.$from.pos; - if (rstate) { - this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); + const editor = this.TextBox.EditorView; + const selection = editor?.state?.selection.$from.pos; + if (editor) { + const estate = editor.state; + editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength)))); } DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); - const fstate = this.TextBox.EditorView?.state; - if (fstate && selection) { - this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); + const teditor = this.TextBox.EditorView; + if (teditor && selection) { + const tstate = teditor.state; + teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection)))); } }; const getTitledDoc = (docTitle: string) => { @@ -326,32 +312,57 @@ export class RichTextRules { const titledDoc = DocServer.FindDocByTitle(docTitle); return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; }; - if (!fieldKey) { - if (docTitle) { - const target = getTitledDoc(docTitle); - if (target) { - setTimeout(() => linkToDoc(target)); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); - } - } - return state.tr; + const target = getTitledDoc(docTitle); + if (target) { + setTimeout(() => linkToDoc(target)); + return state.tr.insertText(' ').deleteRange(start, start + prefixLength); } + } + return state.tr; + }), + + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document + // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value] + // [@{this,doctitle,}.fieldKey] + new InputRule( + new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]*))?\]/), + (state, match, start, end) => { + const docTitle = match[1].substring(1).replace(/\.$/, ''); + const fieldKey = match[2]; + const assign = match[4] === ':' ? (match[4] = '') : match[4]; + const value = match[5]; + const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('='); + const getTitledDoc = (docTitle: string) => { + if (!DocServer.FindDocByTitle(docTitle)) { + Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + } + return DocServer.FindDocByTitle(docTitle); + }; // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); this.Document[DocData][fieldKey] = strs ? new List(values) : new List(values.map(v => Number(v))); } else if (value) { - KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: - (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore + KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined: + (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore } - const target = getTitledDoc(docTitle); - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); + const target = docTitle ? getTitledDoc(docTitle) : undefined; + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, dataDoc }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }, { inCode: true } ), + new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => { + var count = 0; // ignore first return value which will be the notation that chat is pending a result + KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { + count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string))); + count++; + }); + return null; + }), + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // wiki:title new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => { diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index a141ef041..b68acc8f8 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -74,6 +74,7 @@ export const marks: { [index: string]: MarkSpec } = { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, title: { default: null }, noPreview: { default: false }, + fontSize: { default: null }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text }, inclusive: false, @@ -93,14 +94,16 @@ export const marks: { [index: string]: MarkSpec } = { const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); return node.attrs.docref && node.attrs.title ? [ - 'div', + 'a', ['span', 0], [ 'span', { ...node.attrs, class: 'prosemirror-attribution', + 'data-targethrefs': targethrefs, href: node.attrs.allAnchors[0].href, + style: `font-size: ${node.attrs.fontSize}`, }, node.attrs.title, ], diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index c9115be90..905146ee2 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -265,6 +265,8 @@ export const nodes: { [index: string]: NodeSpec } = { docId: { default: '' }, hideKey: { default: false }, editable: { default: true }, + expanded: { default: null }, + dataDoc: { default: false }, }, leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), group: 'inline', diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index daae32e9f..4b40d11b9 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -445,6 +445,12 @@ export namespace Doc { export function GetT(doc: Doc, key: string, ctor: ToConstructor, ignoreProto: boolean = false): FieldResult { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult; } + export function isTemplateDoc(doc: Doc) { + return GetT(doc, 'isTemplateDoc', 'boolean', true); + } + export function isTemplateForField(doc: Doc) { + return GetT(doc, 'isTemplateForField', 'string', true); + } export function IsDataProto(doc: Doc) { return GetT(doc, 'isDataDoc', 'boolean', true); } @@ -642,7 +648,7 @@ export namespace Doc { cloneLinks: boolean, cloneTemplates: boolean ): Promise { - if (Doc.IsBaseProto(doc) || ((Doc.Get(doc, 'isTemplateDoc', true) || Doc.Get(doc, 'isTemplateForField', true)) && !cloneTemplates)) { + if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) { return doc; } if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; @@ -735,7 +741,7 @@ export namespace Doc { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { - clone[key] = !cloneTemplates && (Doc.Get(docAtKey, 'isTemplateDoc', true) || Doc.Get(docAtKey, 'isTemplateForField', true)) ? docAtKey : cloneMap.get(docAtKey[Id]); + clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]); } else { repairClone(docAtKey, cloneMap, cloneTemplates, visited); } @@ -857,7 +863,7 @@ export namespace Doc { // of the original layout while allowing for individual layout properties to be overridden in the expanded layout. export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) { // nothing to do if the layout isn't a template or we don't have a target that's different than the template - if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) { + if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) { return templateLayoutDoc; } @@ -874,7 +880,7 @@ export namespace Doc { expandedTemplateLayout = undefined; _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) { - if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument ?? Doc.GetProto(targetDoc))) { + if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype @@ -910,8 +916,9 @@ export namespace Doc { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc; - return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc }; + const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const templateRoot = DocCast(containerDoc?.rootDocument); + return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc }; } export function FindReferences(infield: Doc | List, references: Set, system: boolean | undefined) { @@ -1035,20 +1042,13 @@ export namespace Doc { // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc). // This is appropriate if you're trying to create a document that behaves like all // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs) - export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc { - const delegateProto = new Doc(); - delegateProto[Initializing] = true; - delegateProto.proto = doc; - delegateProto.author = Doc.CurrentUserEmail; - delegateProto.isDataDoc = true; - title && (delegateProto.title = title); - const delegate = new Doc(id, true); - delegate[Initializing] = true; - delegate.proto = delegateProto; - delegate.author = Doc.CurrentUserEmail; - delegate[Initializing] = false; - delegateProto[Initializing] = false; - return delegate; + export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string) { + const ndoc = Doc.ApplyTemplate(doc); + if (ndoc) { + Doc.GetProto(ndoc).isDataDoc = true; + ndoc && (Doc.GetProto(ndoc).proto = doc); + } + return ndoc; } let _applyCount: number = 0; @@ -1671,7 +1671,7 @@ ScriptingGlobals.add(function getEmbedding(doc: any) { return Doc.MakeEmbedding(doc); }); ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { - return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); + return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto); }); ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); diff --git a/src/fields/util.ts b/src/fields/util.ts index b73520999..c2ec3f13a 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -286,6 +286,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc // target should be either a Doc or ListImpl. receiver should be a Proxy Or List. // export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { + if (!in_prop) { + console.log('WARNING: trying to set an empty property. This should be fixed. '); + return false; + } let prop = in_prop; const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; -- cgit v1.2.3-70-g09d2 From b420caf2c7ecd386cae2cc550904522474b541aa Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 26 Mar 2024 22:34:10 -0400 Subject: added empty image tool and click on empty image to select from filesystem. fixed following links in lightbox and showing links to stackedTimelines. fixed embedding docs into text. fixed not resizing text boxes that also show up in pivot view. prevent context menu from going off top of screen. fixed freeform clustering colors and click to type. fixed links to stackedTimeline marks, and titles for marks. made title editing from doc deco and header use same syntax as keyValue. fixed marquee selection on webBoxes. turn off transitions in freeformdocview after timeout. enabled iconifying templates to propagate to "offspring". fixes images in templates. don't show headr on schema views. --- src/client/documents/Documents.ts | 52 ++++++++-------- src/client/util/DocumentManager.ts | 4 +- src/client/views/ContextMenu.tsx | 6 +- src/client/views/DocumentDecorations.tsx | 71 ++++++++-------------- src/client/views/FieldsDropdown.tsx | 1 - src/client/views/LightboxView.tsx | 2 + src/client/views/MarqueeAnnotator.tsx | 19 ++++-- src/client/views/ObservableReactComponent.tsx | 3 +- src/client/views/PropertiesView.tsx | 2 +- src/client/views/StyleProvider.tsx | 6 +- .../collections/CollectionStackedTimeline.tsx | 13 ++-- .../CollectionFreeFormLayoutEngines.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 11 ++-- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +- .../collectionSchema/SchemaTableCell.tsx | 2 +- src/client/views/global/globalScripts.ts | 4 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 24 ++++++-- src/client/views/nodes/DocumentContentsView.tsx | 7 ++- src/client/views/nodes/DocumentView.tsx | 35 +++++++---- src/client/views/nodes/FieldView.tsx | 1 + src/client/views/nodes/ImageBox.tsx | 55 +++++++++++++---- src/client/views/nodes/KeyValueBox.tsx | 2 +- src/client/views/nodes/LabelBox.tsx | 2 +- src/client/views/nodes/LinkBox.tsx | 47 ++++++++++---- src/client/views/nodes/VideoBox.tsx | 5 +- src/client/views/nodes/WebBox.tsx | 65 ++++++++++++++------ .../views/nodes/formattedText/FormattedTextBox.tsx | 17 ++++-- src/client/views/nodes/trails/PresBox.tsx | 2 +- src/fields/Doc.ts | 29 +++++---- src/fields/PresField.ts | 6 -- src/fields/RichTextField.ts | 7 --- 31 files changed, 311 insertions(+), 195 deletions(-) delete mode 100644 src/fields/PresField.ts (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 17cb6fef8..b63c5e429 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1017,8 +1017,8 @@ export namespace Docs { } export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) { - const imgField = url instanceof ImageField ? url : new ImageField(url); - return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc); + const imgField = url instanceof ImageField ? url : url ? new ImageField(url) : undefined; + return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField?.url.href ?? '-no image-'), ...options }, undefined, undefined, undefined, overwriteDoc); } export function PresDocument(options: DocumentOptions = {}) { @@ -1950,6 +1950,31 @@ export namespace DocUtils { return dd; } + export function assignImageInfo(result: Upload.FileInformation, proto: Doc) { + if (Upload.isImageInformation(result)) { + const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); + const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); + proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); + proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; + proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + if (NumCast(proto['data-nativeOrientation']) >= 5) { + proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; + proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + } + proto.data_exif = JSON.stringify(result.exifData?.data); + proto.data_contentSize = result.contentSize; + // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates + const latitude = result.exifData?.data?.GPSLatitude; + const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; + const longitude = result.exifData?.data?.GPSLongitude; + const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; + if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { + proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); + proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); + } + } + } + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, overwriteDoc?: Doc) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); @@ -1961,28 +1986,7 @@ export namespace DocUtils { if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; - if (Upload.isImageInformation(result)) { - const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); - const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); - proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); - proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; - proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); - if (NumCast(proto['data-nativeOrientation']) >= 5) { - proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; - proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); - } - proto.data_exif = JSON.stringify(result.exifData?.data); - proto.data_contentSize = result.contentSize; - // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates - const latitude = result.exifData?.data?.GPSLatitude; - const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; - const longitude = result.exifData?.data?.GPSLongitude; - const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; - if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { - proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); - proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); - } - } + !(result instanceof Error) && DocUtils.assignImageInfo(result, proto); if (Upload.isVideoInformation(result)) { proto.data_duration = result.duration; } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index a38a330da..40d28c690 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -59,7 +59,7 @@ export class DocumentManager { private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = []; public AddViewRenderedCb = (doc: Opt, func: (dv: DocumentView) => any) => { if (doc) { - const dv = this.getDocumentView(doc); + const dv = LightboxView.LightboxDoc ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc); this._viewRenderedCbs.push({ doc, func }); if (dv) { this.callAddViewFuncs(dv); @@ -262,7 +262,7 @@ export class DocumentManager { return res(this.getDocumentView(docContextPath[0])!); } options.didMove = true; - docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); + (!LightboxView.LightboxDoc && docContextPath.some(doc => TabDocView.Activate(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); this.AddViewRenderedCb(docContextPath[0], dv => res(dv)); })); if (options.openLocation === OpenWhere.lightbox) { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 8f4e43978..ca877b93e 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -218,11 +218,12 @@ export class ContextMenu extends ObservableReactComponent<{}> { this._width = DivWidth(r); this._height = DivHeight(r); } + this._searchRef.current?.focus(); })} style={{ display: this._display ? '' : 'none', left: this.pageX, - ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }), + ...(this._yRelativeToTop ? { top: Math.max(0, this.pageY) } : { bottom: this.pageY }), background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, }}> @@ -265,7 +266,8 @@ export class ContextMenu extends ObservableReactComponent<{}> { const item = this.flatItems[this._selectedIndex]; if (item) { item.event({ x: this.pageX, y: this.pageY }); - } else if (this._searchString.startsWith(this._defaultPrefix)) { + } else { + //if (this._searchString.startsWith(this._defaultPrefix)) { this._defaultItem?.(this._searchString.substring(this._defaultPrefix.length)); } this.closeMenu(); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2fb9f0fc1..4d9b93896 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -34,6 +34,7 @@ import { Colors } from './global/globalEnums'; import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { KeyValueBox } from './nodes/KeyValueBox'; interface DocumentDecorationsProps { PanelWidth: number; @@ -57,7 +58,7 @@ export class DocumentDecorations extends ObservableReactComponent { - if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) { + if (this._accumulatedTitle.startsWith('$')) { this._titleControlString = this._accumulatedTitle; - } else if (this._titleControlString.startsWith('#')) { + } else if (this._titleControlString.startsWith('$')) { if (this._accumulatedTitle.startsWith('-->#')) { SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`))); } @@ -131,26 +132,7 @@ export class DocumentDecorations extends ObservableReactComponent')) { - const title = titleField.toString().replace(/\.?/, ''); - const curKey = Doc.LayoutFieldKey(d.Document); - if (curKey !== title) { - if (title) { - if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') { - d.Document.layout_fieldKey = `layout_${title}`; - d.Document[`layout_${title}`] = FormattedTextBox.LayoutString(title); - d.Document[`${title}_nativeWidth`] = d.Document[`${title}_nativeHeight`] = 0; - } - } else { - d.Document.layout_fieldKey = undefined; - } - } - } else { - Doc.SetInPlace(d.Document, titleFieldKey, titleField, true); - } + KeyValueBox.SetField(d.Document, titleFieldKey, this._accumulatedTitle); }), 'edit title' ); @@ -181,7 +163,8 @@ export class DocumentDecorations extends ObservableReactComponent this.onBackgroundMove(true, e), emptyFunction, action(e => { - !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); + const selected = SelectionManager.Views.length === 1 ? SelectionManager.Docs[0] : undefined; + !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString); this._editingTitle = true; this._keyinput.current && setTimeout(this._keyinput.current.focus); }) @@ -622,11 +605,8 @@ export class DocumentDecorations extends ObservableReactComponent { - this._editingTitle = false; - !hideTitle && this.titleBlur(); - })} - onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} - onKeyDown={hideTitle ? emptyFunction : this.titleEntered} - onPointerDown={e => e.stopPropagation()} - /> + <> + {r - x < 150 ? null : {this._titleControlString + ':'}} + { + this._editingTitle = false; + !hideTitle && this.titleBlur(); + })} + onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} + onKeyDown={hideTitle ? emptyFunction : this.titleEntered} + onPointerDown={e => e.stopPropagation()} + /> + ) : (
      {hideTitle ? null : ( diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx index 5638d34c6..6a5c2cb4c 100644 --- a/src/client/views/FieldsDropdown.tsx +++ b/src/client/views/FieldsDropdown.tsx @@ -61,7 +61,6 @@ export class FieldsDropdown extends ObservableReactComponent filteredOptions.push(pair[0])); const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet })); - console.log(options); return ( + {!this.values.length ? null : ( + - )} */} + )}
      ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 66df1eaf2..3700b08d6 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2064,8 +2064,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this._isHovering = true))} - onPointerLeave={action(() => (this._isHovering = false))} + onPointerEnter={action(() => { + this._isHovering = true; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true); + })} + onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))} ref={r => { this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = r; -- cgit v1.2.3-70-g09d2 From 75cbf3ffc1893d02b48ef65ec834e9ae597398bf Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 1 Apr 2024 20:23:24 -0400 Subject: fixed text toggle buttons to highlight based on selection. enabled background colors for text lists. cleaned up text insertion point setting on pointer up. fixed autoHeight for text boxes with 'auto' nodes like ordered lists. --- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/global/globalScripts.ts | 50 +++--- .../views/nodes/formattedText/DashFieldView.scss | 1 + .../nodes/formattedText/FormattedTextBox.scss | 14 ++ .../views/nodes/formattedText/FormattedTextBox.tsx | 47 ++--- .../formattedText/ProsemirrorExampleTransfer.ts | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 199 ++++----------------- .../views/nodes/formattedText/RichTextRules.ts | 4 +- src/client/views/nodes/formattedText/nodes_rts.ts | 30 ++-- 9 files changed, 113 insertions(+), 236 deletions(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b73111062..38ebc86e7 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -725,7 +725,7 @@ pie title Minerals in my tap water btnList: new List(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}}, - { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, + { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}}, { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index dab642499..497ab98d8 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -151,26 +151,26 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([ ['font', { checkResult: () => RichTextMenu.Instance?.fontFamily, - setDoc: () => value && RichTextMenu.Instance.setFontFamily(value), + setDoc: () => value && RichTextMenu.Instance?.setFontFamily(value), }], ['highlight', { - checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight, - setDoc: () => value && RichTextMenu.Instance.setHighlight(value), + checkResult: () => RichTextMenu.Instance?.fontHighlight, + setDoc: () => value && RichTextMenu.Instance?.setHighlight(value), }], ['fontColor', { checkResult: () => RichTextMenu.Instance?.fontColor, - setDoc: () => value && RichTextMenu.Instance.setColor(value), + setDoc: () => value && RichTextMenu.Instance?.setColor(value), }], ['alignment', { - checkResult: () => RichTextMenu.Instance.textAlign, - setDoc: () => value && editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value), + checkResult: () => RichTextMenu.Instance?.textAlign, + setDoc: () => value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value), }], ['fontSize', { checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), setDoc: () => { if (typeof value === 'number') value = value.toString(); if (value && Number(value).toString() === value) value += 'px'; - RichTextMenu.Instance.setFontSize(value); + RichTextMenu.Instance?.setFontSize(value); }, }], ]); @@ -182,45 +182,45 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh }); type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal'; -type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }]; +type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => any }]; ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) { const textView = RichTextMenu.Instance?.TextView; const editorView = textView?.EditorView; // prettier-ignore const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) => - [ where, { checkResult: () =>(editorView ? (where === 'vcent' ? RichTextMenu.Instance.textVcenter: - (RichTextMenu.Instance.textAlign === where)): + [ where, { checkResult: () =>(editorView ? (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false: + (RichTextMenu.Instance?.textAlign === where)): where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered): (Doc.UserDoc().textAlign ===where) ? true:false), - toggle: () => (editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance.vcenterToggle(editorView, editorView.dispatch): - RichTextMenu.Instance.align(editorView, editorView.dispatch, where)): + toggle: () => (editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(editorView, editorView.dispatch): + RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)): where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered: (Doc.UserDoc().textAlign = where))}]); // prettier-ignore // prettier-ignore const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => - [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false), - toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]); + [ list, { checkResult: () => (editorView ? RichTextMenu.Instance?.getActiveListStyle() === list:false), + toggle: () => editorView?.state && RichTextMenu.Instance?.changeListType(list) }]); // prettier-ignore const attrs:attrfuncs[] = [ ['dictation', { checkResult: () => textView?._recordingDictation ? true:false, toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }], ['elide', { checkResult: () => false, toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}], - ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), + ['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false), toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], - ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}], - ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}], - ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]] + ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold') ? true:false), + toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}], + ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics') ? true:false), + toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}], + ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline') ? true:false), + toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]] const map = new Map(attrs.concat(alignments).concat(listings)); if (checkResult) { return map.get(charStyle)?.checkResult(); } - undoable(() => map.get(charStyle)?.toggle(), 'toggle ' + charStyle)(); + undoable(() => map.get(charStyle)?.toggle?.(), 'toggle ' + charStyle)(); }); export function checkInksToGroup() { @@ -448,10 +448,10 @@ ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { */ ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) { SelectionManager.Docs.map(doc => (doc._text_fontFamily = key)); - const editorView = RichTextMenu.Instance.TextView?.EditorView; + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc())?.fontFamily); } - if (editorView) RichTextMenu.Instance.setFontFamily(key); + if (editorView) RichTextMenu.Instance?.setFontFamily(key); else Doc.UserDoc().fontFamily = key; }); diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 74eeb014c..d79df4272 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -26,6 +26,7 @@ display: inline-block; font-weight: normal; background: rgba(0, 0, 0, 0.1); + cursor: default; } .dashFieldView-fieldSpan { min-width: 8px; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 03ff0436b..3dcc45c96 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -273,6 +273,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; @@ -348,6 +349,7 @@ footnote::before { touch-action: none; span { font-family: inherit; + background-color: inherit; } blockquote { @@ -397,6 +399,7 @@ footnote::before { font-family: inherit; } margin-left: 0; + background-color: inherit; } .decimal2-ol { counter-reset: deci2; @@ -406,6 +409,7 @@ footnote::before { } font-size: smaller; padding-left: 2.1em; + background-color: inherit; } .decimal3-ol { counter-reset: deci3; @@ -415,6 +419,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .decimal4-ol { counter-reset: deci4; @@ -424,6 +429,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } .decimal5-ol { counter-reset: deci5; @@ -432,6 +438,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal6-ol { counter-reset: deci6; @@ -440,6 +447,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal7-ol { counter-reset: deci7; @@ -448,6 +456,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .multi1-ol { @@ -458,6 +467,7 @@ footnote::before { } margin-left: 0; padding-left: 1.2em; + background-color: inherit; } .multi2-ol { counter-reset: multi2; @@ -467,6 +477,7 @@ footnote::before { } font-size: smaller; padding-left: 2em; + background-color: inherit; } .multi3-ol { counter-reset: multi3; @@ -476,6 +487,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .multi4-ol { counter-reset: multi4; @@ -485,6 +497,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } @@ -788,6 +801,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2e8444379..c2f3a6e4b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -291,7 +291,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -1577,32 +1577,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const editor = this._editorView!; - const state = editor?.state; - if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; - if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - else if (this._props.isContentActive() && !e.button) { - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - let xpos = pcords?.pos || 0; - while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { - xpos = xpos - 1; - } - let node: any; - try { - node = state.doc.nodeAt(xpos); - } catch (e) {} - if (node?.type !== schema.nodes.dashFieldView) { - editor.dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(xpos)))); - let target = e.target as any; // hrefs are stored on the dataset of the node that wraps the hyerlink - while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); - } else if (node) { - try { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos)))); - } catch (e) { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos - 1)))); - } - } + const state = this.EditorView?.state; + if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { + if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); + let target = e.target as any; // hrefs are stored on the dataset of the node that wraps the hyerlink + for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); + while (target && !target.dataset?.targethrefs) target = target.parentElement; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); } }; @action @@ -1788,7 +1769,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Number(val.replace('px', '').replace('auto', '0')); - const toHgt = (node: Element) => { + const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; + const toNum = (val: string) => Number(val.replace('px', '')); + const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); - return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); + const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); + return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; - const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); + const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children); const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 92cc65fc5..ec8879487 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -104,7 +104,7 @@ export function buildKeymap>(schema: S, props: any, mapKey !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => { const tx25 = updateBullets(tx2, schema); const ol_node = tx25.doc.nodeAt(range!.start)!; - const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, { ...ol_node.attrs, ...(marks?.[0]?.type === schema.marks.pFontSize ? { fontSize: marks[0].attrs.fontSize } : {}) }); + const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks); // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 9b6eb8b8d..9c282a1d2 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -27,7 +27,10 @@ const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu { - @observable static Instance: RichTextMenu; + static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); + static get Instance() { + return RichTextMenu._instance?.menu; + } public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef(); @@ -67,7 +70,7 @@ export class RichTextMenu extends AntimodeMenu { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu.Instance = this; + RichTextMenu._instance.menu = this; this.updateMenu(undefined, undefined, props, this.layoutDoc); this._canFade = false; this.Pinned = true; @@ -131,11 +134,7 @@ export class RichTextMenu extends AntimodeMenu { if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; } - // update active marks - const activeMarks = this.getActiveMarksOnSelection(); - this.setActiveMarkButtons(activeMarks); - - // update active font family and size + this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); const activeFamilies = active.activeFamilies; const activeSizes = active.activeSizes; @@ -164,15 +163,13 @@ export class RichTextMenu extends AntimodeMenu { const fromRange = numberRange(state.selection.from).reverse(); const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); - if (node?.type === schema.nodes.ordered_list) { - let attrs = node.attrs; - if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; - if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; - const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); - } - { + if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { + const hasMark = node.marks.some(m => m.type === mark.type); + const otherMarks = node.marks.filter(m => m.type !== mark.type); + const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); + const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); + dispatch(updateBullets(markup, state.schema)); + } else { const state = this.view?.state; const tr = this.view?.state.tr; if (tr && state) { @@ -225,11 +222,12 @@ export class RichTextMenu extends AntimodeMenu { if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; - const marks: Mark[] = [...(state.storedMarks ?? [])]; + var marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { } else if (state.selection.empty) { - const ref_node = this.reference_node(pos); - marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; + } } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); @@ -256,41 +254,26 @@ export class RichTextMenu extends AntimodeMenu { //finds all active marks on selection in given group getActiveMarksOnSelection() { - let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; - if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); - //current selection - const { empty, ranges, $to } = this.view.state.selection as TextSelection; const state = this.view.state; - if (!empty) { - activeMarks = markGroup.filter(mark => { - const has = false; - for (let i = 0; !has && i < ranges.length; i++) { - return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark); - } - return false; - }); - } else { - const pos = this.view.state.selection.$from; - const ref_node: ProsNode | null = this.reference_node(pos); - if (ref_node !== null && ref_node !== this.view.state.doc) { - if (ref_node.isText) { - } else { - return []; - } - activeMarks = markGroup.filter(mark_type => { - // if (mark_type === state.schema.marks.pFontSize) { - // return mark.isINSet - // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); - // } - const mark = state.schema.mark(mark_type); - return mark.isInSet(ref_node.marks); - }); + var marks: Mark[] = [...(state.storedMarks ?? [])]; + const pos = this.view.state.selection.$from; + if (state.storedMarks !== null) { + } else if (state.selection.empty) { + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } + } else { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); + }); } - return activeMarks; + const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; + return markGroup.filter(mark_type => { + const mark = state.schema.mark(mark_type); + return mark.isInSet(marks); + }); } @action @@ -418,7 +401,7 @@ export class RichTextMenu extends AntimodeMenu { // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { - const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle(); + const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle(); const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle }); if (!this.view || nodeType?.attrs.mapStyle === '') return; @@ -566,7 +549,7 @@ export class RichTextMenu extends AntimodeMenu { // todo: add brushes to brushMap to save with a style name onBrushNameKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); + RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; @@ -574,7 +557,7 @@ export class RichTextMenu extends AntimodeMenu { @action clearBrush() { - RichTextMenu.Instance.brushMarks = new Set(); + RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set()); } @action @@ -710,118 +693,8 @@ export class RichTextMenu extends AntimodeMenu { } }; - linkExtend($start: ResolvedPos, href: string) { - const mark = this.view!.state.schema.marks.linkAnchor; - - let startIndex = $start.index(); - let endIndex = $start.indexAfter(); - - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++; - - let startPos = $start.start(); - let endPos = startPos; - for (let i = 0; i < endIndex; i++) { - const size = $start.parent.child(i).nodeSize; - if (i < startIndex) startPos += size; - endPos += size; - } - return { from: startPos, to: endPos }; - } - - reference_node(pos: ResolvedPos): ProsNode | null { - if (!this.view) return null; - - let ref_node: ProsNode = this.view.state.doc; - if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { - ref_node = pos.nodeBefore; - } - if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) { - ref_node = pos.nodeAfter; - } - } - if (!ref_node && pos.pos > 0) { - let skip = false; - for (let i: number = pos.pos - 1; i > 0; i--) { - this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { - if (node.isLeaf && !skip) { - ref_node = node; - skip = true; - } - }); - } - } - if (!ref_node.isLeaf && ref_node.childCount > 0) { - ref_node = ref_node.child(0); - } - return ref_node; - } - render() { return null; - // TraceMobx(); - // const row1 =
      {[ - // //!this.collapsed ? this.getDragger() : (null), - // // !this.Pinned ? (null) :
      {[ - // // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // //
      - // // ]}
      , - // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // this.createColorButton(), - // this.createHighlighterButton(), - // this.createLinkButton(), - // this.createBrushButton(), - //
      , - // this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), - // this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), - // this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), - // this.createButton("indent", "Inset More", undefined, this.insetParagraph), - // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), - // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), - // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), - // ]}
      ; - - // const row2 =
      - // {this.collapsed ? this.getDragger() : (null)} - //
      - //
      - // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { - // this.activeFontSize = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val); - // })), - // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { - // this.activeFontFamily = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val); - // })), - //
      , - // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), - // this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), - // this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), - // this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule) - // ]} - //
      - // {/*
      - // {
      - // - //
      } - // - //
      */} - //
      ; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index e8cf9e992..42665830f 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -246,7 +246,7 @@ export class RichTextRules { // activate a style by name using prefix '%' new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => { const color = match[0].substring(1, match[0].length); - const marks = RichTextMenu.Instance._brushMap.get(color); + const marks = RichTextMenu.Instance?._brushMap.get(color); if ( DocListCast((Doc.UserDoc().template_notes as Doc).data) @@ -367,7 +367,7 @@ export class RichTextRules { if (count) { const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); - RichTextMenu.Instance.elideSelection(this.TextBox.EditorView?.state, true); + RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true); } count++; }); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index cab3a6ef5..ceafc3ba1 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -24,6 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = { // :: NodeSpec The top level document node. doc: { content: 'block+', + marks: '_', }, paragraph: ParagraphNodeSpec, @@ -331,12 +332,10 @@ export const nodes: { [index: string]: NodeSpec } = { ...orderedList, content: 'list_item+', group: 'block', + marks: '_', attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet" - fontColor: { default: 'inherit' }, - fontSize: { default: undefined }, - fontFamily: { default: undefined }, + mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet", visibility: { default: true }, indent: { default: undefined }, }, @@ -376,9 +375,10 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; - const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : ''; - const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : ''; - const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : ''; + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ @@ -386,7 +386,7 @@ export const nodes: { [index: string]: NodeSpec } = { { 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `${fsize} ${ffam} ${fcol} ${marg}`, + style: `${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ]; @@ -398,7 +398,7 @@ export const nodes: { [index: string]: NodeSpec } = { class: `${map}-ol`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`, + style: `list-style: none; ${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ] @@ -422,16 +422,22 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, ], - toDOM(node: any) { + toDOM(node: Node) { + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', - { class: `${map}`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, + { class: `${map}`, style: `${fhigh} ${fsize} ${ffam} ${fcol} `, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, node.attrs.visibility ? 0 : [ 'span', - { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre` }, + { + style: `${fhigh} ${fsize} ${ffam} ${fcol} position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre`, + }, `${node.firstChild?.textContent}...`, ], ]; -- cgit v1.2.3-70-g09d2 From ac58c8da5dc8ee5346bccd6da1f6883a7ce0d3d7 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 2 Apr 2024 14:49:15 -0400 Subject: fixed highlight list type button --- src/client/views/global/globalScripts.ts | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src/client/views/global/globalScripts.ts') diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 497ab98d8..2a5732708 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -199,7 +199,7 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: (Doc.UserDoc().textAlign = where))}]); // prettier-ignore // prettier-ignore const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => - [ list, { checkResult: () => (editorView ? RichTextMenu.Instance?.getActiveListStyle() === list:false), + [ list, { checkResult: () => (editorView ? RichTextMenu.Instance?.listStyle === list:false), toggle: () => editorView?.state && RichTextMenu.Instance?.changeListType(list) }]); // prettier-ignore const attrs:attrfuncs[] = [ diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 9c282a1d2..4bd4ca72b 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -51,7 +51,7 @@ export class RichTextMenu extends AntimodeMenu { @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; - @observable private activeListType: string = ''; + @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set = new Set(); @@ -103,6 +103,9 @@ export class RichTextMenu extends AntimodeMenu { @computed get fontSize() { return this._activeFontSize; } + @computed get listStyle() { + return this._activeListType; + } @computed get textAlign() { return this._activeAlignment; } @@ -143,7 +146,7 @@ export class RichTextMenu extends AntimodeMenu { const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); - this.activeListType = this.getActiveListStyle(); + this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; @@ -199,16 +202,15 @@ export class RichTextMenu extends AntimodeMenu { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView?._props.rootSelected?.()) { - const path = (this.view.state.selection.$from as any).path; - for (let i = 0; i < path.length; i += 3) { - if (path[i].type === this.view.state.schema.nodes.ordered_list) { - return path[i].attrs.mapStyle; + const state = this.view?.state; + if (state) { + const pos = state.selection.$anchor; + for (let i = 0; i < pos.depth; i++) { + const node = pos.node(i); + if (node.type === schema.nodes.ordered_list) { + return node.attrs.mapStyle; } } - if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) { - return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle; - } } return ''; } -- cgit v1.2.3-70-g09d2