From f82b0fef26782f6a871da036a6bd9a6670444509 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 27 Jul 2022 15:51:00 -0400 Subject: fixed document leak related to capturedVariables in scripts. changed capturedVariables to be a string encoding the captured variables instead of a Doc. --- src/fields/ScriptField.ts | 169 ++++++++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 73 deletions(-) (limited to 'src/fields/ScriptField.ts') diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 40ca0ce22..2b4b1ef4c 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,29 +1,32 @@ -import { computedFn } from "mobx-utils"; -import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from "serializr"; -import { CompiledScript, CompileScript } from "../client/util/Scripting"; -import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals"; -import { autoObject, Deserializable } from "../client/util/SerializationHelper"; -import { numberRange } from "../Utils"; -import { Doc, Field, Opt } from "./Doc"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { List } from "./List"; -import { ObjectField } from "./ObjectField"; -import { ProxyField } from "./Proxy"; -import { Cast, NumCast } from "./Types"; -import { Plugins } from "./util"; +import { computedFn } from 'mobx-utils'; +import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; +import { DocServer } from '../client/DocServer'; +import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting'; +import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { numberRange } from '../Utils'; +import { Doc, Field, Opt } from './Doc'; +import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; +import { List } from './List'; +import { ObjectField } from './ObjectField'; +import { Cast, NumCast } from './Types'; +import { Plugins } from './util'; function optional(propSchema: PropSchema) { - return custom(value => { - if (value !== undefined) { - return propSchema.serializer(value); - } - return SKIP; - }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { - if (jsonValue !== undefined) { - return propSchema.deserializer(jsonValue, callback, context, oldValue); + return custom( + value => { + if (value !== undefined) { + return propSchema.serializer(value); + } + return SKIP; + }, + (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { + if (jsonValue !== undefined) { + return propSchema.deserializer(jsonValue, callback, context, oldValue); + } + return SKIP; } - return SKIP; - }); + ); } const optionsSchema = createSimpleSchema({ @@ -32,31 +35,19 @@ const optionsSchema = createSimpleSchema({ typecheck: true, editable: true, readonly: true, - params: optional(map(primitive())) + params: optional(map(primitive())), }); const scriptSchema = createSimpleSchema({ options: object(optionsSchema), - originalScript: true + originalScript: true, }); -async function deserializeScript(script: ScriptField) { - const captures: ProxyField = (script as any).captures; - const cache = captures ? undefined : ScriptField.GetScriptFieldCache(script.script.originalScript); - if (cache) return (script as any).script = cache; - if (captures) { - const doc = (await captures.value())!; - const captured: any = {}; - const keys = Object.keys(doc); - const vals = await Promise.all(keys.map(key => doc[key]) as any); - keys.forEach((key, i) => captured[key] = vals[i]); - (script.script.options as any).capturedVariables = captured; - } +function finalizeScript(script: ScriptField, captures: boolean) { const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } - (script as any).script = comp; !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp); if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); @@ -65,10 +56,30 @@ async function deserializeScript(script: ScriptField) { } (script as any).setterscript = compset; } + return comp; +} +async function deserializeScript(script: ScriptField) { + if (script.captures) { + const captured: any = {}; + (script.script.options as ScriptOptions).capturedVariables = captured; + Promise.all( + script.captures.map(async capture => { + const key = capture.split(':')[0]; + const val = capture.split(':')[1]; + if (val === 'true') captured[key] = true; + else if (val === 'false') captured[key] = false; + else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', '')); + else if (!isNaN(Number(val))) captured[key] = Number(val); + else captured[key] = val; + }) + ).then(() => ((script as any).script = finalizeScript(script, true))); + } else { + (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script, false); + } } @scriptingGlobal -@Deserializable("script", deserializeScript) +@Deserializable('script', deserializeScript) export class ScriptField extends ObjectField { @serializable(object(scriptSchema)) readonly script: CompiledScript; @@ -76,18 +87,19 @@ export class ScriptField extends ObjectField { readonly setterscript: CompiledScript | undefined; @serializable(autoObject()) - private captures?: ProxyField; + captures?: List; public static _scriptFieldCache: Map> = new Map(); - public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } + public static GetScriptFieldCache(field: string) { + return this._scriptFieldCache.get(field); + } constructor(script: CompiledScript, setterscript?: CompiledScript) { super(); - if (script?.options.capturedVariables) { - const doc = Doc.assign(new Doc, script.options.capturedVariables); - doc.system = true; - this.captures = new ProxyField(doc); + const captured = script?.options.capturedVariables; + if (captured) { + this.captures = new List(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key].toString()))); } this.setterscript = setterscript; this.script = script; @@ -122,46 +134,45 @@ export class ScriptField extends ObjectField { } [ToScriptString]() { - return "script field"; + return 'script field'; } [ToString]() { return this.script.originalScript; } - public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) { + public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = CompileScript(script, { params: { - this: Doc?.name || "Doc", // this is the doc that executes the script - self: Doc?.name || "Doc", // self is the root doc of the doc that executes the script - _last_: "any", // _last_ is the previous value of a computed field when it is being triggered to re-run. - _readOnly_: "boolean", // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) - ...params + this: Doc?.name || 'Doc', // this is the doc that executes the script + self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script + _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. + _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) + ...params, }, typecheck: false, editable: true, addReturn: addReturn, - capturedVariables + capturedVariables, }); return compiled; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } - public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } } @scriptingGlobal -@Deserializable("computed", deserializeScript) +@Deserializable('computed', deserializeScript) export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result; - + _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript); @@ -171,7 +182,7 @@ export class ComputedField extends ScriptField { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ComputedField(compiled) : undefined; } @@ -182,7 +193,7 @@ export class ComputedField extends ScriptField { doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); - const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: "any" }, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } @@ -196,7 +207,7 @@ export namespace ComputedField { useComputed = true; } - export const undefined = "__undefined"; + export const undefined = '__undefined'; export function WithoutComputed(fn: () => T) { DisableComputedFields(); @@ -216,15 +227,27 @@ export namespace ComputedField { } } -ScriptingGlobals.add(function setIndexVal(list: any[], index: number, value: any) { - while (list.length <= index) list.push(undefined); - list[index] = value; -}, "sets the value at a given index of a list", "(list: any[], index: number, value: any)"); - -ScriptingGlobals.add(function getIndexVal(list: any[], index: number) { - return list?.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); - -ScriptingGlobals.add(function makeScript(script: string) { - return ScriptField.MakeScript(script); -}, "returns the value at a given index of a list", "(list: any[], index: number)"); +ScriptingGlobals.add( + function setIndexVal(list: any[], index: number, value: any) { + while (list.length <= index) list.push(undefined); + list[index] = value; + }, + 'sets the value at a given index of a list', + '(list: any[], index: number, value: any)' +); + +ScriptingGlobals.add( + function getIndexVal(list: any[], index: number) { + return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), undefined as any); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); + +ScriptingGlobals.add( + function makeScript(script: string) { + return ScriptField.MakeScript(script); + }, + 'returns the value at a given index of a list', + '(list: any[], index: number)' +); -- cgit v1.2.3-70-g09d2 From cfa31b87f1c2a6597ed3cfb6a777126c58ace665 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 2 Aug 2022 16:28:18 -0400 Subject: Adjusted ScriptFields to have a rawScript, and updated ScrptingBoxes to create a scriptField even for scripts that don't compile. Updated CurrentUserUtils setup functions for clicks. Fixed TemplateMenu to work again. --- src/client/documents/Documents.ts | 14 +-- src/client/util/CurrentUserUtils.ts | 88 +++++++------- src/client/views/DocumentButtonBar.tsx | 15 ++- src/client/views/ScriptBox.tsx | 134 +++++++++++---------- src/client/views/TemplateMenu.tsx | 98 ++++++--------- .../views/collections/CollectionStackingView.tsx | 2 +- src/client/views/nodes/DocumentView.scss | 23 ++-- src/client/views/nodes/DocumentView.tsx | 22 +++- src/client/views/nodes/FieldView.tsx | 5 +- src/client/views/nodes/ScriptingBox.tsx | 85 +++++++------ src/client/views/nodes/button/FontIconBox.tsx | 6 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 8 +- src/fields/ScriptField.ts | 11 +- 13 files changed, 259 insertions(+), 252 deletions(-) (limited to 'src/fields/ScriptField.ts') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 51a9283f4..64d26e425 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -264,12 +264,6 @@ export class DocumentOptions { baseProto?: boolean; // is this a base prototoype dontRegisterView?: boolean; lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. - 'onDoubleClick-rawScript'?: string; // onDoubleClick script in raw text form - 'onChildDoubleClick-rawScript'?: string; // onChildDoubleClick script in raw text form - 'onChildClick-rawScript'?: string; // on ChildClick script in raw text form - 'onClick-rawScript'?: string; // onClick script in raw text form - 'onCheckedClick-rawScript'?: string; // onChecked script in raw text form - 'onCheckedClick-params'?: List; // parameter list for onChecked treeview functions columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view clipWidth?: number; // percent transition from before to after in comparisonBox @@ -1065,7 +1059,7 @@ export namespace Docs { } export function ButtonDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), 'onClick-rawScript': '-script-' }); + return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) }); } export function SliderDocument(options?: DocumentOptions) { @@ -1356,7 +1350,11 @@ export namespace DocUtils { scripts && Object.keys(scripts).map(key => { if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && scripts[key]) { - doc[key] = ScriptField.MakeScript(scripts[key], { dragData: DragManager.DocumentDragData.name, value: 'any', scriptContext: 'any', documentView: Doc.name }, { _readOnly_: true }); + doc[key] = ScriptField.MakeScript( + scripts[key], + { dragData: DragManager.DocumentDragData.name, value: 'any', scriptContext: 'any', thisContainer: Doc.name, documentView: Doc.name, heading: Doc.name, checked: 'boolean', containingTreeView: Doc.name }, + { _readOnly_: true } + ); } }); funcs && diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ce32595d4..f55ed6e72 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -97,6 +97,45 @@ export class CurrentUserUtils { return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); } + /// Initializes templates for editing click funcs of a document + static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") { + const tempClicks = DocCast(doc[field]); + const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true}; + const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ + { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"}, + { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}]; + const reqdClickList = reqdTempOpts.map(opts => { + const allOpts = {...reqdClickOpts, ...opts.opts}; + const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; + return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script,allOpts)); + }); + + const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true}; + return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); + } + + /// Initializes templates for editing click funcs of a document + static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") { + const tempClicks = DocCast(doc[field]); + const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true}; + const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ + { opts: { title: "onClick"}, script: "console.log( 'click')"}, + { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"}, + { opts: { title: "onChildClick"}, script: "console.log( 'child click')"}, + { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"}, + { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"}, + ]; + const reqdClickList = reqdTempOpts.map(opts => { + const allOpts = {...reqdClickOpts, ...opts.opts}; + const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; + return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title); + }); + + const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true}; + return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); + } + + /// Initializes templates that can be applied to notes static setupNoteTemplates(doc: Doc, field="template-notes") { const tempNotes = DocCast(doc[field]); @@ -105,7 +144,7 @@ export class CurrentUserUtils { { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; const reqdNoteList = reqdTempOpts.map(opts => { - const reqdOpts = {...opts, title: "text", system: true}; + const reqdOpts = {...opts, title: "text", width:200, system: true}; const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); }); @@ -118,10 +157,10 @@ export class CurrentUserUtils { static setupDocTemplates(doc: Doc, field="myTemplates") { DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"}); const templates = [ - DocCast(doc.presElement), CurrentUserUtils.setupNoteTemplates(doc), CurrentUserUtils.setupClickEditorTemplates(doc) ]; + CurrentUserUtils.setupChildClickEditors(doc) const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); @@ -748,51 +787,6 @@ export class CurrentUserUtils { DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; } - - static setupClickEditorTemplates(doc: Doc) { - if (doc["clickFuncs-child"] === undefined) { - // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target - const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( - "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ", - { thisContainer: Doc.name }), { - title: "Click to open in target", _width: 300, _height: 200, - targetScriptKey: "onChildClick", system: true - }); - - const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "openOnRight(self.doubleClickView)", {}), - { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick", system: true }); - - doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates", system: true }); - } - - if (doc.clickFuncs === undefined) { - const onClick = Docs.Create.ScriptingDocument(undefined, { - title: "onClick", "onClick-rawScript": "console.log('click')", - isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200, system: true - }, "onClick"); - const onChildClick = Docs.Create.ScriptingDocument(undefined, { - title: "onChildClick", "onChildClick-rawScript": "console.log('child click')", - isTemplateDoc: true, isTemplateForField: "onChildClick", _width: 300, _height: 200, system: true - }, "onChildClick"); - const onDoubleClick = Docs.Create.ScriptingDocument(undefined, { - title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')", - isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200, system: true - }, "onDoubleClick"); - const onChildDoubleClick = Docs.Create.ScriptingDocument(undefined, { - title: "onChildDoubleClick", "onChildDoubleClick-rawScript": "console.log('child double click')", - isTemplateDoc: true, isTemplateForField: "onChildDoubleClick", _width: 300, _height: 200, system: true - }, "onChildDoubleClick"); - const onCheckedClick = Docs.Create.ScriptingDocument(undefined, { - title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", - "onCheckedClick-params": new List(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, - isTemplateForField: "onCheckedClick", _width: 300, _height: 200, system: true - }, "onCheckedClick"); - doc.clickFuncs = Docs.Create.TreeDocument([onClick, onChildClick, onDoubleClick, onCheckedClick], { title: "onClick funcs", system: true }); - } - - return doc.clickFuncs as Doc; - } - /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field /// whether to revert to "default" values, or to leave them as the user/system last set them. diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index bac51a11d..1d4056759 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -335,8 +335,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ); } @observable _aliasDown = false; - onAliasButtonDown = action((e: React.PointerEvent): void => { - this.props.views()[0]?.select(false); + onTemplateButton = action((e: React.PointerEvent): void => { this._tooltipOpen = false; setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); }); @@ -374,7 +373,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ) }> -
+
{}
@@ -412,15 +411,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
) : null} - {/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) :
- {this.templateButton} -
- /*
+ { + Doc.noviceMode ? null :
{this.templateButton}
+ /*
{this.metadataButton}
{this.contextButton} -
*/} +
*/ + } {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null :
{this.followLinkButton}
}
{this.pinButton}
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null :
{this.shareButton}
} diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index b7ea124b9..416162aeb 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -1,17 +1,16 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, Opt } from "../../fields/Doc"; -import { ScriptField } from "../../fields/ScriptField"; -import { ScriptCast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; -import { DragManager } from "../util/DragManager"; -import { CompileScript } from "../util/Scripting"; -import { EditableView } from "./EditableView"; -import { DocumentIconContainer } from "./nodes/DocumentIcon"; -import { OverlayView } from "./OverlayView"; -import "./ScriptBox.scss"; - +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, Opt } from '../../fields/Doc'; +import { ScriptField } from '../../fields/ScriptField'; +import { ScriptCast } from '../../fields/Types'; +import { emptyFunction } from '../../Utils'; +import { DragManager } from '../util/DragManager'; +import { CompileScript } from '../util/Scripting'; +import { EditableView } from './EditableView'; +import { DocumentIconContainer } from './nodes/DocumentIcon'; +import { OverlayView } from './OverlayView'; +import './ScriptBox.scss'; export interface ScriptBoxProps { onSave: (text: string, onError: (error: string) => void) => void; @@ -28,53 +27,58 @@ export class ScriptBox extends React.Component { constructor(props: ScriptBoxProps) { super(props); - this._scriptText = props.initialText || ""; + this._scriptText = props.initialText || ''; } @action onChange = (e: React.ChangeEvent) => { this._scriptText = e.target.value; - } + }; @action onError = (error: string) => { - console.log("ScriptBox: " + error); - } + console.log('ScriptBox: ' + error); + }; overlayDisposer?: () => void; onFocus = () => { this.overlayDisposer?.(); this.overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); - } + }; onBlur = () => { this.overlayDisposer?.(); - } + }; render() { - let onFocus: Opt<() => void> = undefined, onBlur: Opt<() => void> = undefined; + let onFocus: Opt<() => void> = undefined, + onBlur: Opt<() => void> = undefined; if (this.props.showDocumentIcons) { onFocus = this.onFocus; onBlur = this.onBlur; } - const params = ""} - SetValue={(value: string) => this.props.setParams && this.props.setParams(value.split(" ").filter(s => s !== " ")) ? true : true} - />; + const params = ''} SetValue={(value: string) => (this.props.setParams?.(value.split(' ').filter(s => s !== ' ')) ? true : true)} />; return (
-
+
-
{params}
+
{params}
- - + +
); @@ -90,35 +94,43 @@ export class ScriptBox extends React.Component { // tslint:disable-next-line: no-unnecessary-callback-wrapper const params: string[] = []; const setParams = (p: string[]) => params.splice(0, params.length, ...p); - const scriptingBox = { - if (!text) { - Doc.GetProto(doc)[fieldKey] = undefined; - } else { - const script = CompileScript(text, { - params: { this: Doc.name, ...contextParams }, - typecheck: false, - editable: true, - transformer: DocumentIconContainer.getTransformer() - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } + const scriptingBox = ( + { + if (!text) { + Doc.GetProto(doc)[fieldKey] = undefined; + } else { + const script = CompileScript(text, { + params: { this: Doc.name, ...contextParams }, + typecheck: false, + editable: true, + transformer: DocumentIconContainer.getTransformer(), + }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join('\n')); + return; + } - const div = document.createElement("div"); - div.style.width = "90"; - div.style.height = "20"; - div.style.background = "gray"; - div.style.position = "absolute"; - div.style.display = "inline-block"; - div.style.transform = `translate(${clientX}px, ${clientY}px)`; - div.innerHTML = "button"; - params.length && DragManager.StartButtonDrag([div], text, doc.title + "-instance", {}, params, (button: Doc) => { }, clientX, clientY); + const div = document.createElement('div'); + div.style.width = '90'; + div.style.height = '20'; + div.style.background = 'gray'; + div.style.position = 'absolute'; + div.style.display = 'inline-block'; + div.style.transform = `translate(${clientX}px, ${clientY}px)`; + div.innerHTML = 'button'; + params.length && DragManager.StartButtonDrag([div], text, doc.title + '-instance', {}, params, (button: Doc) => {}, clientX, clientY); - Doc.GetProto(doc)[fieldKey] = new ScriptField(script); - overlayDisposer(); - } - }} showDocumentIcons />; + Doc.GetProto(doc)[fieldKey] = new ScriptField(script); + overlayDisposer(); + } + }} + showDocumentIcons + /> + ); overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title }); } } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 156513f47..863829a51 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -55,16 +55,12 @@ export class TemplateMenu extends React.Component { @observable private _hidden: boolean = true; toggleLayout = (e: React.ChangeEvent, layout: string): void => { - this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout)); + this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout, undefined, true)); }; toggleDefault = (e: React.ChangeEvent): void => { this.props.docViews.map(dv => dv.switchViews(false, 'layout')); }; - toggleAudio = (e: React.ChangeEvent): void => { - this.props.docViews.map(dv => (dv.props.Document._showAudio = e.target.checked)); - }; - @undoBatch @action toggleTemplate = (event: React.ChangeEvent, template: string): void => { @@ -76,12 +72,6 @@ export class TemplateMenu extends React.Component { this._hidden = !this._hidden; }; - @undoBatch - @action - toggleChrome = (): void => { - this.props.docViews.map(dv => Doc.Layout(dv.layoutDoc)).forEach(layout => (layout._chromeHidden = !layout._chromeHidden)); - }; - // todo: add brushes to brushMap to save with a style name onCustomKeypress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -95,13 +85,9 @@ export class TemplateMenu extends React.Component { .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); } - return100 = () => 100; + return100 = () => 300; @computed get scriptField() { - const script = ScriptField.MakeScript( - 'docs.map(d => switchView(d, this))', - { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name, firstDoc: Doc.name }, - { docs: new List(this.props.docViews.map(dv => dv.props.Document)) } - ); + const script = ScriptField.MakeScript('docs.map(d => switchView(d, this))', { this: Doc.name }, { docs: this.props.docViews.map(dv => dv.props.Document) as any }); return script ? () => script : undefined; } templateIsUsed = (selDoc: Doc, templateDoc: Doc) => { @@ -113,13 +99,10 @@ export class TemplateMenu extends React.Component { const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layoutKey, 'layout').replace('layout_', ''); const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const addedTypes = Doc.noviceMode ? [] : DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data); - const layout = Doc.Layout(firstDoc); + const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); const templateMenu: Array = []; this.props.templates?.forEach((checked, template) => templateMenu.push()); - templateMenu.push(); templateMenu.push(); - !Doc.noviceMode && templateMenu.push(); addedTypes.concat(noteTypes).map(template => (template.treeViewChecked = this.templateIsUsed(firstDoc, template))); this._addedKeys && Array.from(this._addedKeys) @@ -129,43 +112,42 @@ export class TemplateMenu extends React.Component {
    {Doc.noviceMode ? null : } {templateMenu} - {Doc.noviceMode ? null : ( - - )} +
); } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6850fb23a..a6c367ff7 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -296,7 +296,7 @@ export class CollectionStackingView extends CollectionSubView.documentView-node { + > .documentView-node { position: absolute; } } @@ -158,7 +158,7 @@ top: 0; width: 100%; height: 14; - background: rgba(0, 0, 0, .4); + background: rgba(0, 0, 0, 0.4); text-align: center; text-overflow: ellipsis; white-space: pre; @@ -187,19 +187,18 @@ transition: opacity 0.5s; } } - } .documentView-node:hover, .documentView-node-topmost:hover { - >.documentView-styleWrapper { - >.documentView-titleWrapper-hover { + > .documentView-styleWrapper { + > .documentView-titleWrapper-hover { display: inline-block; } } - >.documentView-styleWrapper { - >.documentView-captionWrapper { + > .documentView-styleWrapper { + > .documentView-captionWrapper { opacity: 1; } } @@ -225,6 +224,6 @@ .documentView-node:first-child { position: relative; - background: "#B59B66"; //$white; + background: '#B59B66'; //$white; } -} \ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8847c0c6a..edaa40bad 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -177,6 +177,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling NativeWidth?: () => number; NativeHeight?: () => number; + NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps LayoutTemplate?: () => Opt; contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; onClick?: () => ScriptField; @@ -191,7 +192,6 @@ export interface DocumentViewProps extends DocumentViewSharedProps { export interface DocumentViewInternalProps extends DocumentViewProps { NativeWidth: () => number; NativeHeight: () => number; - NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps isSelected: (outsideReaction?: boolean) => boolean; select: (ctrlPressed: boolean) => void; DocumentView: () => DocumentView; @@ -716,7 +716,7 @@ export class DocumentViewInternal extends DocComponent this.props.removeDocument?.(this.props.Document); - @undoBatch setToggleDetail = () => (this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace('layout_', '')}")`, { documentView: 'any' })); + @undoBatch setToggleDetail = () => + (this.Document.onClick = ScriptField.MakeScript( + `toggleDetail(documentView, "${StrCast(this.Document.layoutKey) + .replace('layout_', '') + .replace(/^layout$/, 'detail')}")`, + { documentView: 'any' } + )); @undoBatch @action @@ -1538,11 +1544,15 @@ export class DocumentView extends React.Component { Doc.setNativeView(this.props.Document); custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); }; - switchViews = action((custom: boolean, view: string, finished?: () => void) => { + switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc setTimeout( action(() => { - this.setCustomView(custom, view); + if (useExistingLayout && custom && this.rootDoc['layout_' + view]) { + this.rootDoc.layoutKey = 'layout_' + view; + } else { + this.setCustomView(custom, view); + } this.docView && (this.docView._animateScalingTo = 1); // expand it setTimeout( action(() => { @@ -1646,7 +1656,7 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); - else dv.switchViews(true, detailLayoutKeySuffix); + else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 5a6c49809..dd2c13391 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -4,10 +4,9 @@ import { observer } from 'mobx-react'; import { DateField } from '../../../fields/DateField'; import { Doc, Field, FieldResult, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { WebField } from '../../../fields/URLField'; -import { DocumentView, DocumentViewSharedProps } from './DocumentView'; import { ScriptField } from '../../../fields/ScriptField'; -import { RecordingBox } from './RecordingBox'; +import { WebField } from '../../../fields/URLField'; +import { DocumentViewSharedProps } from './DocumentView'; // // these properties get assigned through the render() method of the DocumentView when it creates this node. diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 05ff40f22..1c9b0bc0e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -6,7 +6,7 @@ import { Doc } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; @@ -60,6 +60,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent !p.startsWith('_')) + .map(key => key + ':' + params[key]); + } + } } // vars included in fields that store parameters types and names and the script itself @@ -70,30 +78,30 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent p.split(':')[1].trim()); } @computed({ keepAlive: true }) get rawScript() { - return StrCast(this.dataDoc[this.props.fieldKey + '-rawScript'], ''); + return ScriptCast(this.rootDoc[this.fieldKey])?.script.originalScript ?? ''; } @computed({ keepAlive: true }) get functionName() { - return StrCast(this.dataDoc[this.props.fieldKey + '-functionName'], ''); + return StrCast(this.rootDoc[this.props.fieldKey + '-functionName'], ''); } @computed({ keepAlive: true }) get functionDescription() { - return StrCast(this.dataDoc[this.props.fieldKey + '-functionDescription'], ''); + return StrCast(this.rootDoc[this.props.fieldKey + '-functionDescription'], ''); } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.props.fieldKey + '-params'], listSpec('string'), []); + return Cast(this.rootDoc[this.props.fieldKey + '-params'], listSpec('string'), []); } set rawScript(value) { - this.dataDoc[this.props.fieldKey + '-rawScript'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey, new ScriptField(undefined, undefined, value), true); } set functionName(value) { - this.dataDoc[this.props.fieldKey + '-functionName'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionName', value, true); } set functionDescription(value) { - this.dataDoc[this.props.fieldKey + '-functionDescription'] = value; + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionDescription', value, true); } set compileParams(value) { - this.dataDoc[this.props.fieldKey + '-params'] = new List(value); + Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-params', new List(value), true); } getValue(result: any, descrip: boolean) { @@ -107,8 +115,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent { const area = document.querySelector('textarea'); @@ -171,13 +178,13 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); - const result = CompileScript(this.rawScript, { + const result = CompileScript(this.rawText, { editable: true, transformer: DocumentIconContainer.getTransformer(), params, typecheck: false, }); - this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result) : undefined; + Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true); this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; @@ -187,9 +194,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent { if (this.onCompile()) { const bindings: { [name: string]: any } = {}; - this.paramsNames.forEach(key => (bindings[key] = this.dataDoc[key])); + this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key])); // binds vars so user doesnt have to refer to everything as self. - ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); + ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); } }; @@ -257,14 +264,14 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent { - this.dataDoc[fieldKey] = de.complete.docDragData?.droppedDocuments[0]; + Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true); e.stopPropagation(); }; // deletes a param from all areas in which it is stored @action onDelete = (num: number) => { - this.dataDoc[this.paramsNames[num]] = undefined; + Doc.SetInPlace(this.rootDoc, this.paramsNames[num], undefined, true); this.compileParams.splice(num, 1); return true; }; @@ -274,7 +281,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent { //@ts-ignore const val = e.target.selectedOptions[0].value; - this.dataDoc[name] = val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true'; + Doc.SetInPlace(this.rootDoc, name, val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true', true); }; // creates a copy of the script document @@ -330,8 +337,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent this.dataDoc[parameter]?.title ?? 'undefined'} + contents={StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} + GetValue={() => StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')} SetValue={action((value: string) => { const script = CompileScript(value, { addReturn: true, @@ -341,7 +348,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent e.stopPropagation()} onChange={e => this.viewChanged(e, parameter)} - value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> + value={typeof this.rootDoc[parameter] === 'string' ? 'S' + StrCast(this.rootDoc[parameter]) : typeof this.rootDoc[parameter] === 'number' ? 'N' + NumCast(this.rootDoc[parameter]) : 'B' + BoolCast(this.rootDoc[parameter])}> {types.map(type => (