diff options
36 files changed, 273 insertions, 152 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ee3bf0d82..f1a0b37b3 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -211,7 +211,7 @@ export class DocumentOptions { author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); annotationOn?: DOCt = new DocInfo('document annotated by this document', false); - _embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument', false); + _embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false); rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)'); color?: STRt = new StrInfo('foreground color data doc', false); hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 6ddb65941..8f46f844c 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -61,7 +61,7 @@ export let resolvedPorts: { server: number, socket: number }; export class CurrentUserUtils { // initializes experimental advanced template views - slideView, headerView - static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) { + static setupExperimentalTemplateButtons(doc: Doc, tempDocs:Opt<Doc>, userBtns:Doc[]) { const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [ { btnOpts: { title: "slide", icon: "address-card" }, @@ -83,7 +83,7 @@ export class CurrentUserUtils { ) }, ]; - const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => { + const requiredTypes = [...requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => { const tempBtn = DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title); const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' }; const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => { @@ -94,7 +94,7 @@ export class CurrentUserUtils { return templateBtn; }; return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts); - }); + }), ...userBtns]; const reqdOpts:DocumentOptions = { title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, @@ -201,7 +201,7 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}), makeIconTemplate(DocumentType.PDF, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}), makeIconTemplate(DocumentType.WEB, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}), - makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}), + makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "title"}), makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), @@ -465,7 +465,9 @@ export class CurrentUserUtils { static setupToolsBtnPanel(doc: Doc, field:string) { const myTools = DocCast(doc[field]); const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined); - const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined); + const tempBtns = DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined; + const userTemplateBtns = DocListCast(tempBtns?.data).filter(btn => !btn.isSystem); + const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc, tempBtns, userTemplateBtns); const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", layout_explainer: "This is a palette of documents that can be created.", diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f62ec8f83..8c3b56452 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -80,6 +80,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { title: StrCast(layoutDoc.title), btnType: ButtonType.ClickButton, icon: 'bolt', + isSystem: false, }); dbox.dragFactory = layoutDoc; dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 9a6719ea5..421855bf3 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -102,7 +102,7 @@ export namespace UndoManager { 'UndoEvent : ' + event.prop + ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)) + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) ); currentBatch.push(event); tempEvents?.push(event); diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 3c9d821a9..b2076e1a5 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -38,16 +38,16 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & componentDidMount() { runInAction(() => (this._items.length = 0)); - if ((this.props as SubmenuProps)?.subitems) { - (this.props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); + if ((this._props as SubmenuProps)?.subitems) { + (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); } } handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { - if ('event' in this.props) { - this.props.closeMenu?.(); - const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined; - await this.props.event({ x: e.clientX, y: e.clientY }); + if ('event' in this._props) { + this._props.closeMenu?.(); + const batch = this._props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this._props.description}`) : undefined; + await this._props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } }; @@ -91,11 +91,11 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & } render() { - if ('event' in this.props) { + if ('event' in this._props) { return ( - <div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> - {this.props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this.props.icon) ? this.props.icon : <FontAwesomeIcon icon={this.props.icon} size="sm" />}</span> : null} - <div className="contextMenu-description">{this.props.description.replace(':', '')}</div> + <div className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> + {this._props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this._props.icon) ? this._props.icon : <FontAwesomeIcon icon={this._props.icon} size="sm" />}</span> : null} + <div className="contextMenu-description">{this._props.description.replace(':', '')}</div> <div className={`contextMenu-item-background`} style={{ @@ -104,7 +104,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & /> </div> ); - } else if ('subitems' in this.props) { + } else if ('subitems' in this._props) { const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center'; const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : ''; @@ -118,32 +118,32 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & background: SettingsManager.userBackgroundColor, }}> {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> ))} </div> ); - if (!(this.props as SubmenuProps).noexpand) { + if (!(this._props as SubmenuProps).noexpand) { return ( <div className="contextMenu-inlineMenu"> {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> ))} </div> ); } return ( <div - className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} - style={{ alignItems: where, borderTop: this.props.addDivider ? 'solid 1px' : undefined }} + className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} + style={{ alignItems: where, borderTop: this._props.addDivider ? 'solid 1px' : undefined }} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> - {this.props.icon ? ( + {this._props.icon ? ( <span className="contextMenu-item-icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: 'center', alignSelf: 'center' }}> - <FontAwesomeIcon icon={this.props.icon} size="sm" /> + <FontAwesomeIcon icon={this._props.icon} size="sm" /> </span> ) : null} <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}> - {this.props.description} + {this._props.description} <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> <div diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b21b13e4c..73fa6709c 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -188,8 +188,8 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); - Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc); + Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true); + Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc, true); doc.embedContainer = undefined; if (recent && !dontAddToRemoved) { doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); @@ -243,7 +243,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { inheritParentAcls(targetDataDoc, doc, true); }); - const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; + const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them. maybe we should copy them over, though... if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add))); else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 5ef62b2c5..2193acf62 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -18,7 +18,6 @@ import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; -import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -112,7 +111,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora @action titleBlur = () => { - this._editingTitle = false; if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) { this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith('#')) { @@ -616,7 +614,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (SelectionManager.Views.length === 1) { const selected = SelectionManager.Views[0]; if (this._titleControlString.startsWith('=')) { - return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || ''; + return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })?.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || ''; } if (this._titleControlString.startsWith('#')) { return Field.toString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-'; @@ -641,7 +639,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const { b, r, x, y } = this.Bounds; const seldocview = SelectionManager.Views.lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { - setTimeout(action(() => (this._showNothing = true))); + setTimeout( + action(() => { + this._editingTitle = false; + this._showNothing = true; + }) + ); return null; } @@ -726,7 +729,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora name="dynbox" autoComplete="on" value={hideTitle ? '' : this._accumulatedTitle} - onBlur={e => !hideTitle && this.titleBlur()} + onBlur={action((e: React.FocusEvent) => { + this._editingTitle = false; + !hideTitle && this.titleBlur(); + })} onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} onKeyDown={hideTitle ? emptyFunction : this.titleEntered} onPointerDown={e => e.stopPropagation()} diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index c6666d79d..4508d00a7 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as Autosuggest from 'react-autosuggest'; @@ -97,9 +97,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> { super.componentDidUpdate(prevProps); if (this._editing && this._props.editing === false) { this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); - } else if (this._props.editing !== undefined) { - this._editing = this._props.editing; - } + } else + runInAction(() => { + if (this._props.editing !== undefined) this._editing = this._props.editing; + }); } componentWillUnmount() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 7d3acaea7..9e7d364ea 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -176,6 +176,11 @@ // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. touch-action: none; transform-origin: top left; + > svg { + height: 100%; + width: 100%; + margin: auto; + } .collectionfreeformview-placeholder { background: gray; @@ -270,34 +275,31 @@ padding: 10px; .msg { - position: relative; - // display: block; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; - + position: relative; + // display: block; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; } - + .gif-container { - position: relative; - margin-top: 5px; - // display: block; - - justify-content: center; - align-items: center; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; - - + position: relative; + margin-top: 5px; + // display: block; + + justify-content: center; + align-items: center; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; + .gif { background-color: transparent; height: 300px; } } - } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 53dc389b4..be19fc50f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -39,7 +39,7 @@ import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; -import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView'; +import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../../nodes/trails/PresBox'; import { CreateImage } from '../../nodes/WebBoxRenderer'; @@ -73,6 +73,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')'; } // this makes mobx trace() statements more descriptive + @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); + @computed get paintFunc() { + const paintFunc = StrCast(this.Document[DocData].paintFunc).trim(); + return !paintFunc + ? '' + : `const dashDiv = document.querySelector('#${this._paintedId}'); + (async () => { ${paintFunc} })()`; + } constructor(props: any) { super(props); makeObservable(this); @@ -1227,6 +1235,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onKey={this.onKeyDown} onDoubleClickScript={this.onChildDoubleClickHandler} onBrowseClickScript={this.onBrowseClickHandler} + bringToFront={this.bringToFront} ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf} PanelWidth={childLayout[Width]} PanelHeight={childLayout[Height]} @@ -1482,6 +1491,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); }) ); + + this._disposers.paintFunc = reaction( + () => ({ code: this.paintFunc, first: this._firstRender, width: this.Document._width, height: this.Document._height }), + ({ code, first }) => code && !first && eval(code), + { fireImmediately: true } + ); + this._disposers.layoutElements = reaction( // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters () => this.doInternalLayoutComputation, @@ -1847,6 +1863,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return ( <div className="collectionfreeformview-container" + id={this._paintedId} ref={r => { this.createDashEventsTarget(r); this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); @@ -1868,7 +1885,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: `${100 / (this.nativeDimScaling || 1)}%`, height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, }}> - {this._lightboxDoc ? ( + {this.paintFunc ? null : this._lightboxDoc ? ( <div style={{ padding: 15, width: '100%', height: '100%' }}> <DocumentView {...this._props} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 581425d77..31b4a2dd4 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -25,6 +25,7 @@ import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; +const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export enum ColumnType { Number, @@ -69,7 +70,7 @@ export class CollectionSchemaView extends CollectionSubView() { public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; - public static _newNodeInputHeight: number = 20; + public static _newNodeInputHeight: number = Number(SCHEMA_NEW_NODE_HEIGHT); public fieldInfos = new ObservableMap<string, FInfo>(); @observable _menuKeys: string[] = []; diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss index 44e8efe23..ad0c5c21d 100644 --- a/src/client/views/global/globalCssVariables.module.scss +++ b/src/client/views/global/globalCssVariables.module.scss @@ -64,6 +64,7 @@ $mainTextInput-zindex: 999; // then text input overlay so that it's context menu $docDecorations-zindex: 998; // then doc decorations appear over everything else $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? $SCHEMA_DIVIDER_WIDTH: 4; +$SCHEMA_NEW_NODE_HEIGHT: 30; $MINIMIZED_ICON_SIZE: 24; $MAX_ROW_HEIGHT: 44px; $DFLT_IMAGE_NATIVE_DIM: 900px; @@ -78,6 +79,7 @@ $DATA_VIZ_TABLE_ROW_HEIGHT: 30; :export { contextMenuZindex: $contextMenu-zindex; + SCHEMA_NEW_NODE_HEIGHT: $SCHEMA_NEW_NODE_HEIGHT; SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; diff --git a/src/client/views/global/globalCssVariables.module.scss.d.ts b/src/client/views/global/globalCssVariables.module.scss.d.ts index bcbb1f068..12cc3fc89 100644 --- a/src/client/views/global/globalCssVariables.module.scss.d.ts +++ b/src/client/views/global/globalCssVariables.module.scss.d.ts @@ -1,5 +1,6 @@ interface IGlobalScss { contextMenuZindex: string; // context menu shows up over everything + SCHEMA_NEW_NODE_HEIGHT: string; SCHEMA_DIVIDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index dc4aee1ca..ad6deeefb 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -136,7 +136,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this._props.linkDoc)))); @observable _hover = false; - docView = () => this.props.docView; + docView = () => this._props.docView; render() { const destinationIcon = Doc.toIcon(this._props.destinationDoc) as any as IconProp; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 8365f4770..e453bcee0 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; -import { ObservableMap, action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils'; @@ -40,9 +40,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _marqueeref = React.createRef<MarqueeAnnotator>(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _disposers: { [name: string]: IReactionDisposer } = {}; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; - @observable _schemaDataVizChildren: any = undefined; @observable _marqueeing: number[] | undefined = undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -256,12 +256,39 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im componentDidMount() { this._props.setContentViewBox?.(this); if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); + this._disposers.datavis = reaction( + () => { + const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); + const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); + if (!keys) return; + const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); + var current: { [key: string]: string }[] = []; + children + .filter(child => child) + .forEach(child => { + const row: { [key: string]: string } = {}; + keys.forEach(key => { + var cell = child[key]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + row[key] = StrCast(cell); + }); + current.push(row); + }); + return current; + }, + current => { + current && DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current); + }, + { fireImmediately: true } + ); } fetchData = () => { - DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + if (!this.Document.dataViz_asSchema) { + DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests + fetch('/csvData?uri=' + this.dataUrl?.url.href) // + .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + } }; // toggles for user to decide which chart type to view the data in @@ -327,33 +354,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im GPTPopup.Instance.addDoc = this.sidebarAddDocument; }; - @action - updateSchemaViz = () => { - const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); - const keys = Cast(getFrom.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); - if (!keys) return; - const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); - var current: { [key: string]: string }[] = []; - for (let i = 0; i < children.length; i++) { - var row: { [key: string]: string } = {}; - if (children[i]) { - for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); - row[keys[j]] = StrCast(cell); - } - } - current.push(row); - } - DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current); - }; - render() { - if (this.layoutDoc && this.layoutDoc.dataViz_asSchema) { - this._schemaDataVizChildren = DocListCast(DocCast(this.layoutDoc.dataViz_asSchema)[Doc.LayoutFieldKey(DocCast(this.layoutDoc.dataViz_asSchema))]).length; - this.updateSchemaViz(); - } - const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 2f7dd0487..116a45623 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -93,7 +93,6 @@ display: flex; flex-direction: column; cursor: default; - margin-top: 30px; height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows .tableBox-container { overflow: scroll; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a82580ddb..2e1ba2a7e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -592,13 +592,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); if (!this.Document.annotationOn) { - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index cf07d98be..8290e102c 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -73,7 +73,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { }; specificContextMenu = (): void => { - if (!Doc.noviceMode) { + 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' }); @@ -366,7 +366,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { ); } - render() { + renderButton = () => { const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const tooltip = StrCast(this.Document.toolTip); const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false }); @@ -389,5 +389,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } return this.defaultButton; + }; + + render() { + return <div onContextMenu={this.specificContextMenu}>{this.renderButton()}</div>; } } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index dc9914637..18286267a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { Doc } from '../../../../fields/Doc'; +import { Doc, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; @@ -114,6 +114,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } } + componentDidMount() { + this._reactionDisposer = reaction( + () => (this._dashDoc ? Field.toKeyValueString(this._dashDoc, this._props.fieldKey) : undefined), + keyvalue => keyvalue && this._props.tbox.tryUpdateDoc(true) + ); + } + componentWillUnmount() { this._reactionDisposer?.(); } @@ -184,7 +191,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }}> {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {this._fieldKey} + {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey} </span> )} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 731ab1d53..6f9c2893e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -67,6 +67,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; +import { CollectionView } from '../../collections/CollectionView'; // import * as applyDevTools from 'prosemirror-dev-tools'; @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -325,13 +326,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } catch (e) {} }; + leafText = (node: Node) => { + if (node.type === this._editorView?.state.schema.nodes.dashField) { + const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + return Field.toJavascriptString(refDoc[node.attrs.fieldKey as string] as Field); + } + return ''; + }; dispatchTransaction = (tx: Transaction) => { if (this._editorView && (this._editorView as any).docView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + this.tryUpdateDoc(false); + } + }; + tryUpdateDoc = (force: boolean) => { + if (this._editorView && (this._editorView as any).docView) { + const state = this._editorView.state; const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; - const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); + const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template @@ -350,13 +364,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined; let unchanged = true; - if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) { this._applyingChange = this.fieldKey; const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited @@ -480,7 +494,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) { + if (!sel.$anchor.pos || [autoLinkTerm, StrCast(target.title)].includes(editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim())) { tr = tr.addMark(sel.from, sel.to, splitter); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { @@ -919,6 +933,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); + optionItems.push({ + description: 'Make Paint Function', + event: () => { + this.dataDoc.layout_painted = CollectionView.LayoutString('painted'); + this.layoutDoc.layout_fieldKey = 'layout_painted'; + this.layoutDoc.type_collection = CollectionViewType.Freeform; + this.DocumentView?.().setToggleDetail(); + this.dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${this.fieldKey}']?.Text)`); + }, + icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', + }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, @@ -1682,7 +1707,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.LiveTextUndo = undefined; const state = this._editorView!.state; - const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) { UndoManager.RunInBatch(() => { this.dataDoc.title_custom = true; diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index d023020e1..31f001b11 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; +import { DocServer } from '../../../DocServer'; +import { Doc, Field } from '../../../../fields/Doc'; const blockquoteDOM: DOMOutputSpec = ['blockquote', 0], hrDOM: DOMOutputSpec = ['hr'], @@ -264,6 +266,7 @@ export const nodes: { [index: string]: NodeSpec } = { hideKey: { default: false }, editable: { default: true }, }, + leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), group: 'inline', draggable: false, toDOM(node) { diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts index 84917ae53..870dfbeac 100644 --- a/src/fields/CursorField.ts +++ b/src/fields/CursorField.ts @@ -1,6 +1,6 @@ import { createSimpleSchema, object, serializable } from 'serializr'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, FieldChanged, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; export type CursorPosition = { @@ -55,6 +55,9 @@ export default class CursorField extends ObjectField { return new CursorField(this.data); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { return 'invalid'; } diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index 2ea619bd9..1e5222fb6 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, date } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; @scriptingGlobal @@ -23,6 +23,9 @@ export class DateField extends ObjectField { return `${this.date.toLocaleString()}`; } + [ToJavascriptString]() { + return `"${this.date.toISOString()}"`; + } [ToScriptString]() { return `new DateField(new Date("${this.date.toISOString()}"))`; } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ff416bbe7..f4141cf46 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -18,7 +18,7 @@ import { DocAcl, DocCss, DocData, DocFields, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight, Initializing, Self, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore -import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { InkField, InkTool } from './InkField'; import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; @@ -52,6 +52,23 @@ export namespace Field { default: return field?.[ToScriptString]?.() ?? 'null'; } // prettier-ignore } + export function toJavascriptString(field: Field) { + var rawjava = ''; + + switch (typeof field) { + case 'string': + case 'number': + case 'boolean':rawjava = String(field); + break; + default: rawjava = field?.[ToJavascriptString]?.() ?? 'null'; + } // prettier-ignore + var script = rawjava; + Doc.MyPublishedDocs.forEach(doc => { + const regex = new RegExp(`^\\^${doc.title}\\s`, 'm'); + script = script.replace(regex, Cast(doc.text, RichTextField, null)?.Text ?? ''); + }); + return script; + } export function toString(field: Field) { if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); return field?.[ToString]?.() || ''; @@ -287,6 +304,7 @@ export class Doc extends RefField { public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [Width] = () => NumCast(this[SelfProxy]._width); public [Height] = () => NumCast(this[SelfProxy]._height); + public [ToJavascriptString] = () => `idToDoc("${this[Self][Id]}")`; // what should go here? public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } // prettier-ignore @@ -508,12 +526,9 @@ export namespace Doc { * Removes doc from the list of Docs at listDoc[fieldKey] * @returns true if successful, false otherwise. */ - export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) { + export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) { const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); - if (listDoc[key] === undefined) { - listDoc[DocData][key] = new List<Doc>(); - } - const list = Cast(listDoc[key], listSpec(Doc)); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); if (ind !== -1) { @@ -528,12 +543,9 @@ export namespace Doc { * Adds doc to the list of Docs stored at listDoc[fieldKey]. * @returns true if successful, false otherwise. */ - export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) { + export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) { const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); - if (listDoc[key] === undefined) { - listDoc[DocData][key] = new List<Doc>(); - } - const list = Cast(listDoc[key], listSpec(Doc)); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { const pind = list.findIndex(d => d instanceof Doc && d[Id] === doc[Id]); @@ -1020,7 +1032,7 @@ export namespace Doc { // export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean { // find the metadata field key that this template field doc will display (indicated by its title) - const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, ''); + const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField); // update the original template to mark it as a template templateField.isTemplateForField = metadataFieldKey; @@ -1640,3 +1652,6 @@ ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: a ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); +ScriptingGlobals.add(function toJavascriptString(str: string) { + return Field.toJavascriptString(str as Field); +}); diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index 0dbeb064b..0c44d2417 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -5,5 +5,6 @@ export const Parent = Symbol('FieldParent'); export const Copy = Symbol('FieldCopy'); export const ToValue = Symbol('FieldToValue'); export const ToScriptString = Symbol('FieldToScriptString'); +export const ToJavascriptString = Symbol('FieldToJavascriptString'); export const ToPlainText = Symbol('FieldToPlainText'); export const ToString = Symbol('FieldToString'); diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts index 6e8bba977..b67f0f7e9 100644 --- a/src/fields/HtmlField.ts +++ b/src/fields/HtmlField.ts @@ -1,9 +1,9 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString} from "./FieldSymbols"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -@Deserializable("html") +@Deserializable('html') export class HtmlField extends ObjectField { @serializable(primitive()) readonly html: string; @@ -17,8 +17,11 @@ export class HtmlField extends ObjectField { return new HtmlField(this.html); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { return this.html; diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts index 76c4ddf1b..4d2badb68 100644 --- a/src/fields/IconField.ts +++ b/src/fields/IconField.ts @@ -1,9 +1,9 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -@Deserializable("icon") +@Deserializable('icon') export class IconField extends ObjectField { @serializable(primitive()) readonly icon: string; @@ -17,10 +17,13 @@ export class IconField extends ObjectField { return new IconField(this.icon); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { - return "ICONfield"; + return 'ICONfield'; } } diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 22bea3927..b3e01229a 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -2,7 +2,7 @@ import { Bezier } from 'bezier-js'; import { alias, createSimpleSchema, list, object, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; // Helps keep track of the current ink tool in use. @@ -85,6 +85,9 @@ export class InkField extends ObjectField { return new InkField(this.inkData); } + [ToJavascriptString]() { + return '[' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + ']'; + } [ToScriptString]() { return 'new InkField([' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + '])'; } diff --git a/src/fields/List.ts b/src/fields/List.ts index b8ad552d2..9458a9611 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -5,7 +5,7 @@ import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; import { Field } from './Doc'; import { FieldTuples, Self, SelfProxy } from './DocSymbols'; -import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -310,6 +310,9 @@ class ListImpl<T extends Field> extends ObjectField { private [Self] = this; private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references + [ToJavascriptString]() { + return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; + } [ToScriptString]() { return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; } diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index b5bc2952a..e1b5b036c 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,5 +1,5 @@ import { RefField } from './RefField'; -import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols'; +import { FieldChanged, Parent, Copy, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Field } from './Doc'; @@ -12,6 +12,7 @@ export abstract class ObjectField { public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; + abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; abstract [ToString](): string; } diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 3a46e3581..820d9b6ff 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -4,7 +4,7 @@ import { DocServer } from '../client/DocServer'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; import { Field, FieldWaiting, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { RefField } from './RefField'; @@ -38,6 +38,9 @@ export class ProxyField<T extends RefField> extends ObjectField { return new ProxyField<T>(this.fieldId); } + [ToJavascriptString]() { + return Field.toScriptString(this[ToValue](undefined)?.value); + } [ToScriptString]() { return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts index b6ef69750..01828dd14 100644 --- a/src/fields/RefField.ts +++ b/src/fields/RefField.ts @@ -1,11 +1,11 @@ -import { serializable, primitive, alias } from "serializr"; -import { Utils } from "../Utils"; -import { Id, HandleUpdate, ToScriptString, ToString } from "./FieldSymbols"; -import { afterDocDeserialize } from "../client/util/SerializationHelper"; +import { serializable, primitive, alias } from 'serializr'; +import { Utils } from '../Utils'; +import { Id, HandleUpdate, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; +import { afterDocDeserialize } from '../client/util/SerializationHelper'; export type FieldId = string; export abstract class RefField { - @serializable(alias("id", primitive({ afterDeserialize: afterDocDeserialize }))) + @serializable(alias('id', primitive({ afterDeserialize: afterDocDeserialize }))) private __id: FieldId; readonly [Id]: FieldId; @@ -16,6 +16,7 @@ export abstract class RefField { protected [HandleUpdate]?(diff: any): void | Promise<void>; + abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; abstract [ToString](): string; } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 3e75a071f..50cfab988 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -1,7 +1,7 @@ import { serializable } from 'serializr'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; @scriptingGlobal @@ -27,6 +27,9 @@ export class RichTextField extends ObjectField { return new RichTextField(this.Data, this.Text); } + [ToJavascriptString]() { + return '`' + this.Text + '`'; + } [ToScriptString]() { return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`; } diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 6dde2e5aa..fb4dc4e5b 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols'; +import { Copy, ToScriptString, ToString, FieldChanged, ToJavascriptString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; @@ -114,6 +114,9 @@ export class SchemaHeaderField extends ObjectField { return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed); } + [ToJavascriptString]() { + return `["${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed}]`; + } [ToScriptString]() { return `schemaHeaderField("${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed})`; } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 62690a9fb..c7fe72ca6 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -6,7 +6,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; import { Doc, Field, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; @@ -113,6 +113,9 @@ export class ScriptField extends ObjectField { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } + [ToJavascriptString]() { + return this.script.originalScript; + } [ToScriptString]() { return this.script.originalScript; } diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 817b62373..87334ad16 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, custom } from 'serializr'; import { ObjectField } from './ObjectField'; -import { ToScriptString, ToString, Copy } from './FieldSymbols'; +import { ToScriptString, ToString, Copy, ToJavascriptString } from './FieldSymbols'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Utils } from '../Utils'; @@ -36,6 +36,12 @@ export abstract class URLField extends ObjectField { } return `new ${this.constructor.name}("${this.url?.href}")`; } + [ToJavascriptString]() { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { + return `new ${this.constructor.name}("${this.url.pathname}")`; + } + return `new ${this.constructor.name}("${this.url?.href}")`; + } [ToString]() { if (Utils.prepend(this.url?.pathname) === this.url?.href) { return this.url.pathname; |