diff options
| author | Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com> | 2022-06-30 12:28:25 -0700 |
|---|---|---|
| committer | Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com> | 2022-06-30 12:28:25 -0700 |
| commit | 891b03238ee7bf6bd9e83cc20c1720c42059ce04 (patch) | |
| tree | b912f6f1e8177106ce015896882cdc8bb11acafd /src/client/util | |
| parent | bb02d3a052efdbf25d1069059a92b7a9d9cc1708 (diff) | |
| parent | ea6e63648b21c46672b1b7cb1da0cbaa6857d0c1 (diff) | |
Merge branch 'master' into parker
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 1926 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 123 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 70 | ||||
| -rw-r--r-- | src/client/util/DropConverter.ts | 7 | ||||
| -rw-r--r-- | src/client/util/History.ts | 8 | ||||
| -rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/util/InteractionUtils.tsx | 2 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 151 | ||||
| -rw-r--r-- | src/client/util/RecordingApi.ts | 269 | ||||
| -rw-r--r-- | src/client/util/Scripting.ts | 2 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.tsx | 40 | ||||
| -rw-r--r-- | src/client/util/SharingManager.tsx | 35 | ||||
| -rw-r--r-- | src/client/util/SnappingManager.ts | 4 |
13 files changed, 1283 insertions, 1356 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 0db8bebd8..a39081d10 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,6 +1,6 @@ import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; @@ -8,29 +8,26 @@ import { PrefetchProxy } from "../../fields/Proxy"; import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types"; +import { Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { SharingPermissions } from "../../fields/util"; -import { Utils } from "../../Utils"; +import { OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; -import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; +import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; import { DocumentType } from "../documents/DocumentTypes"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { TreeViewType } from "../views/collections/CollectionTreeView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; import { TreeView } from "../views/collections/TreeView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; -import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; -import { LabelBox } from "../views/nodes/LabelBox"; import { OverlayView } from "../views/OverlayView"; -import { DocumentManager } from "./DocumentManager"; import { DragManager } from "./DragManager"; -import { makeTemplate } from "./DropConverter"; +import { MakeTemplate } from "./DropConverter"; import { HistoryUtil } from "./History"; import { LinkManager } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; -import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { ColorScheme } from "./SettingsManager"; import { SharingManager } from "./SharingManager"; @@ -38,30 +35,32 @@ import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; interface Button { + // DocumentOptions fields a button can set title?: string; toolTip?: string; icon?: string; btnType?: ButtonType; - click?: string; numBtnType?: NumButtonType; numBtnMin?: number; numBtnMax?: number; switchToggle?: boolean; - script?: string; width?: number; - list?: string[]; + btnList?: List<string>; ignoreClick?: boolean; buttonText?: string; + + // fields that do not correspond to DocumentOption fields + scripts?: { script?: string; onClick?: string; } + funcs?: { [key:string]: string }; + subMenu?: Button[]; } export let resolvedPorts: { server: number, socket: number }; -const headerViewVersion = "0.1"; export class CurrentUserUtils { private static curr_id: string; //TODO tfs: these should be temporary... private static mainDocId: string | undefined; - public static searchBtn: Doc; public static get id() { return this.curr_id; } public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } @@ -71,557 +70,325 @@ export class CurrentUserUtils { @observable public static GuestDashboard: Doc | undefined; @observable public static GuestMobile: Doc | undefined; @observable public static propertiesWidth: number = 0; + @observable public static headerBarHeight: number = 0; @observable public static searchPanelWidth: number = 0; - // sets up the default User Templates - slideView, headerView - static setupUserTemplateButtons(doc: Doc) { - // Prototype for mobile button (not sure if 'Advanced Item Prototypes' is ideal location) - if (doc["template-mobile-button"] === undefined) { - const queryTemplate = this.mobileButton({ - title: "NEW MOBILE BUTTON", - onClick: undefined, - }, - [this.createToolButton({ - ignoreClick: true, - icon: "mobile", - btnType: ButtonType.ToolButton, - backgroundColor: "transparent" - }), - this.mobileTextContainer({}, - [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])]); - doc["template-mobile-button"] = CurrentUserUtils.createToolButton({ - onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile", btnType: ButtonType.ToolButton, - }); - } - - if (doc["template-button-slides"] === undefined) { - const slideTemplate = Docs.Create.MultirowDocument( - [ - Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), - Docs.Create.TextDocument("", { title: "text", _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) }) - ], - { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true } - ); - slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); - doc["template-button-slides"] = CurrentUserUtils.createToolButton({ - onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card", - btnType: ButtonType.ToolButton + static AssignScripts(doc:Doc, scripts?:{ [key: string]: string;}, funcs?:{[key:string]: string}) { + 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}); + } + }); + funcs && Object.keys(funcs).map(key => { + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + if (ScriptCast(cfield)?.script.originalScript !== funcs[key] && funcs[key]) { + doc[key] = ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, {"_readOnly_": true}); + } + }); + return doc; + } + static AssignOpts(doc:Doc|undefined, reqdOpts:DocumentOptions, items?:Doc[]) { + if (doc) { + const compareValues = (val1:any, val2:any) => { + if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) { + return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v)); + } + return val1 === val2; + } + Object.entries(reqdOpts).forEach(pair => { + const targetDoc = pair[0].startsWith("_") ? doc : Doc.GetProto(doc as Doc); + if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/,"")) || + !compareValues(pair[1], targetDoc[pair[0]])) { + targetDoc[pair[0]] = pair[1]; + } }); + items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), "data", item)); + items && DocListCast(doc.data).forEach(item => !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), "data", item)); } + return doc; + } + static AssignDocField(doc:Doc, field:string, creator:(reqdOpts:DocumentOptions, items?:Doc[]) => Doc, reqdOpts:DocumentOptions, items?: Doc[], scripts?:{[key:string]:string}, funcs?:{[key:string]:string}) { + return this.AssignScripts(this.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); + } - if (doc["template-button-link"] === undefined) { // set _backgroundColor to transparent to prevent link dot from obscuring document it's attached to. - const linkTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _autoHeight: true, system: true }, "header")); // text needs to be a space to allow templateText to be created - linkTemplate.system = true; - Doc.GetProto(linkTemplate).layout = - "<div>" + - " <FormattedTextBox {...props} dontSelectOnLoad={'true'} height='{this._headerHeight||75}px' ignoreAutoHeight={'true'} background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" + - " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + - "</div>"; - (linkTemplate.proto as Doc).isTemplateDoc = makeTemplate(linkTemplate.proto as Doc, true, "linkView"); - - const rtf2 = { - doc: { - type: "doc", content: [ - { - type: "paragraph", - content: [{ - type: "dashField", - attrs: { - fieldKey: "src", - hideKey: false - } - }] - }, - { type: "paragraph" }, - { - type: "paragraph", - content: [{ - type: "dashField", - attrs: { - fieldKey: "dst", - hideKey: false - } - }] - }] - }, - selection: { type: "text", anchor: 1, head: 1 }, - storedMarks: [] + // initializes experimental advanced template views - slideView, headerView + static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) { + const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [ + { + btnOpts: { title: "slide", icon: "address-card" }, + templateOpts: { _width: 400, _height: 300, title: "slideView", childDocumentsActive: true, _xMargin: 3, _yMargin: 3, system: true }, + template: (opts:DocumentOptions) => Docs.Create.MultirowDocument( + [ + Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), + Docs.Create.TextDocument("", { title: "text", _fitWidth:true, _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) }) + ], opts) + }, + { + btnOpts: { title: "mobile", icon: "mobile" }, + templateOpts: { title: "NEW MOBILE BUTTON", onClick: undefined, }, + template: (opts:DocumentOptions) => this.mobileButton(opts, + [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }), + this.mobileTextContainer({}, + [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")]) + ] + ) + }, + ]; + 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); }' }; + const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => { + if (templateBtn) { + this.AssignOpts(templateBtn,btnOpts); + this.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions); + } + return templateBtn; }; - linkTemplate.header = new RichTextField(JSON.stringify(rtf2), ""); - - doc["template-button-link"] = CurrentUserUtils.createToolButton({ - onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true, - btnType: ButtonType.ToolButton - }); - } + return this.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts); + }); - // if (doc["template-button-switch"] === undefined) { - // const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; - - // const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40, system: true }); - // const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1, system: true }); - // const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, system: true }); - // const labelTemplate = { - // doc: { - // type: "doc", content: [{ - // type: "paragraph", - // content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }] - // }] - // }, - // selection: { type: "text", anchor: 1, head: 1 }, - // storedMarks: [] - // }; - // Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS"); - // Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'"); - // // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'"); - // // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true"); - // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]"); - // // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false"); - // const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, system: true }); - // box.isTemplateDoc = makeTemplate(box, true, "switch"); - - // doc["template-button-switch"] = CurrentUserUtils.createToolButton({ - // onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - // dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true, - // btnType: ButtonType.ToolButton - // }); - // } - - const requiredTypes = [ - doc["template-button-slides"] as Doc, - doc["template-mobile-button"] as Doc, - doc["template-button-detail"] as Doc, - doc["template-button-link"] as Doc, - //doc["template-button-switch"] as Doc] - ]; - if (doc["template-buttons"] === undefined) { - doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { - title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _chromeHidden: true, - hidden: ComputedField.MakeFunction("IsNoviceMode()") as any, - _stayInCollection: true, _hideContextMenu: true, _forceActive: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true - })); - } else { - const curButnTypes = Cast(doc["template-buttons"], Doc, null); - DocListCastAsync(curButnTypes.data).then(async curBtns => { - curBtns && await Promise.all(curBtns); - requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); - }); - } - return doc["template-buttons"] as Doc; + const reqdOpts:DocumentOptions = { + title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true, + _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, childDocumentsActive: true, + _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, + }; + const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; + const reqdFuncs = { hidden: "IsNoviceMode()" }; + return this.AssignScripts(this.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); } - // setup the different note type skins - static setupNoteTemplates(doc: Doc) { - if (doc["template-note-Note"] === undefined) { - const noteView = Docs.Create.TextDocument("", { - title: "text", isTemplateDoc: true, backgroundColor: "yellow", system: true, icon: "sticky-note", - _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), - }); - noteView.isTemplateDoc = makeTemplate(noteView, true, "Note"); - doc["template-note-Note"] = new PrefetchProxy(noteView); - } - if (doc["template-note-Idea"] === undefined) { - const noteView = Docs.Create.TextDocument("", { - title: "text", backgroundColor: "pink", system: true, icon: "lightbulb", - _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), - }); - noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea"); - doc["template-note-Idea"] = new PrefetchProxy(noteView); - } - if (doc["template-note-Topic"] === undefined) { - const noteView = Docs.Create.TextDocument("", { - title: "text", backgroundColor: "lightblue", system: true, icon: "book-open", - _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), - }); - noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic"); - doc["template-note-Topic"] = new PrefetchProxy(noteView); - } - // if (doc["template-note-Todo"] === undefined) { - // const noteView = Docs.Create.TextDocument("", { - // title: "text", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption", - // layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus"), system: true, - // _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), - // }); - // noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo"); - // doc["template-note-Todo"] = new PrefetchProxy(noteView); - // } - // const taskStatusValues = [ - // { title: "todo", _backgroundColor: "blue", color: "white", system: true }, - // { title: "in progress", _backgroundColor: "yellow", color: "black", system: true }, - // { title: "completed", _backgroundColor: "green", color: "white", system: true } - // ]; - // if (doc.fieldTypes === undefined) { - // doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations", system: true }); - // DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues); - // } - - if (doc["template-notes"] === undefined) { - doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc], // doc["template-note-Todo"] as any as Doc], - { title: "Note Layouts", _height: 75, system: true })); - } else { - const curNoteTypes = Cast(doc["template-notes"], Doc, null); - const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc]; - DocListCastAsync(curNoteTypes.data).then(async curNotes => { - curNotes && await Promise.all(curNotes); - requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype)); - }); - } + /// Initializes templates that can be applied to notes + static setupNoteTemplates(doc: Doc, field="template-notes") { + const tempNotes = DocCast(doc[field]); + const reqdTempOpts:DocumentOptions[] = [ + { noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"}, + { 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 noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; + return this.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); + }); - return doc["template-notes"] as Doc; + const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true }; + return this.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } - // creates Note templates, and initial "user" templates - static setupDocTemplates(doc: Doc) { - const noteTemplates = CurrentUserUtils.setupNoteTemplates(doc); - const userTemplateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); - const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc); - if (doc.templateDocs === undefined) { - doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], { - title: "template layouts", _xMargin: 0, system: true, - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) - })); - } + /// Initializes collection of templates for notes and click functions + static setupDocTemplates(doc: Doc, field="myTemplates") { + this.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) + ]; + const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, }; + const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; + return this.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } // setup templates for different document types when they are iconified from Document Decorations - static setupDefaultIconTemplates(doc: Doc) { - if (doc["template-icon-view"] === undefined) { - const iconView = Docs.Create.LabelDocument({ - title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimgray", - _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true - }); - // Docs.Create.TextDocument("", { - // title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") - // }); - // Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); - iconView.isTemplateDoc = makeTemplate(iconView); - doc["template-icon-view"] = new PrefetchProxy(iconView); - } - if (doc["template-icon-view-rtf"] === undefined) { - const iconRtfView = Docs.Create.LabelDocument({ - title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"), - _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true - }); - iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); - doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView); - } - if (doc["template-icon-view-button"] === undefined) { - const iconBtnView = Docs.Create.FontIconDocument({ - title: "icon_" + DocumentType.BUTTON, _nativeHeight: 30, _nativeWidth: 30, - _width: 30, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true - }); - iconBtnView.isTemplateDoc = makeTemplate(iconBtnView, true, "icon_" + DocumentType.BUTTON); - doc["template-icon-view-button"] = new PrefetchProxy(iconBtnView); - } - if (doc["template-icon-view-img"] === undefined) { - const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { - title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true - }); - iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG); - doc["template-icon-view-img"] = new PrefetchProxy(iconImageView); - } - if (doc["template-icon-view-col"] === undefined) { - const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true }); - iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL); - doc["template-icon-view-col"] = new PrefetchProxy(iconColView); - } - if (doc["template-icons"] === undefined) { - doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75, system: true })); - } else { - const templateIconsDoc = Cast(doc["template-icons"], Doc, null); - const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; - DocListCastAsync(templateIconsDoc.data).then(async curIcons => { - curIcons && await Promise.all(curIcons); - requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); - }); - } - return doc["template-icons"] as Doc; + static setupDefaultIconTemplates(doc: Doc, field="template-icons") { + const reqdOpts = { title: "icon templates", _height: 75, system: true }; + const templateIconsDoc = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); + + const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => { + const iconFieldName = "icon" + (type ? "_" + type : ""); + const curIcon = DocCast(templateIconsDoc[iconFieldName]); + let creator = labelBox; + switch (opts.iconTemplate) { + case DocumentType.IMG : creator = imageBox; break; + case DocumentType.FONTICON: creator = fontBox; break; + } + const allopts = {system: true, ...opts}; + return this.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? + this.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), + {onClick:"deiconifyView(documentView)"}); + }; + const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ + textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + }); + const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _showTitle: "title", ...opts }); + const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); + const iconTemplates = [ + makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}), + 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, _showTitle: "creationDate"}), + makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), + makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), + makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}), + makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}), + //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now + makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }), + //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts)) + ].filter(d => d).map(d => d!); + this.AssignOpts(DocCast(doc[field]), {}, iconTemplates); } + /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation static creatorBtnDescriptors(doc: Doc): { - title: string, toolTip: string, icon: string, drag?: string, ignoreClick?: boolean, - click?: string, backgroundColor?: string, dragFactory?: Doc, noviceMode?: boolean, clickFactory?: Doc + title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc, + backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string}, }[] { - //TODO: we may need to add in note-teking view here - if (doc.emptyPresentation === undefined) { - doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), - { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _fitWidth: true, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyPresentation as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyCollection === undefined) { - doc.emptyCollection = Docs.Create.FreeformDocument([], - { _nativeWidth: undefined, _nativeHeight: undefined, _fitWidth: true, _width: 150, _height: 100, title: "freeform", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyCollection as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyPane === undefined) { - doc.emptyPane = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _backgroundGridShow: true, _nativeHeight: undefined, _width: 500, _height: 800, title: "Untitled Tab", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptySlide === undefined) { - const textDoc = Docs.Create.TreeDocument([], { - title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, - allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, - backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) - }); - Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); - FormattedTextBox.SelectOnLoad = textDoc[Id]; - doc.emptySlide = textDoc; - } - if ((doc.emptyHeader as Doc)?.version !== headerViewVersion) { - const json = { - doc: { - type: "doc", - content: [ - { - type: "paragraph", attrs: {}, content: [{ - type: "dashField", - attrs: { fieldKey: "author", docid: "", hideKey: false }, - marks: [{ type: "strong" }] - }, { - type: "dashField", - attrs: { fieldKey: "creationDate", docid: "", hideKey: false }, - marks: [{ type: "strong" }] - }] + const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) }); + const json = { + doc: { + type: "doc", + content: [ + { + type: "paragraph", attrs: {}, content: [{ + type: "dashField", + attrs: { fieldKey: "author", docid: "", hideKey: false }, + marks: [{ type: "strong" }] + }, { + type: "dashField", + attrs: { fieldKey: "creationDate", docid: "", hideKey: false }, + marks: [{ type: "strong" }] }] - }, - selection: { type: "text", anchor: 1, head: 1 }, - storedMarks: [] - }; - const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { - title: "text", version: headerViewVersion, _height: 70, _headerPointerEvents: "all", - _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, _fitWidth: true, - cloneFieldFilter: new List<string>(["system"]) - }, "header"); - const headerBtnHgt = 10; - headerTemplate[DataSym].layout = + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + const headerBtnHgt = 10; + const headerTemplate = (opts:DocumentOptions) => { + const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", + layout: "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + - "</HTMLdiv>"; + "</HTMLdiv>" + }, "header"); // "<div style={'height:100%'}>" + // " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" + // " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + // "</div>"; - (headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView"); - doc.emptyHeader = headerTemplate; - ((doc.emptyHeader as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyComparison === undefined) { - doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "comparison box", _width: 300, _height: 300, system: true, cloneFieldFilter: new List<string>(["system"]) }); - } - if (doc.emptyScript === undefined) { - doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyScreenshot === undefined) { - doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, title: "empty screenshot", _width: 400, _height: 200, system: true, cloneFieldFilter: new List<string>(["system"]) }); - } - if (doc.emptyWall === undefined) { - doc.emptyWall = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); - (doc.emptyWall as Doc).videoWall = true; - } - if (doc.emptyAudio === undefined) { - doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0; + MakeTemplate(Doc.GetProto(header), true, "Untitled Header"); + return header; } - if (doc.emptyNote === undefined) { - doc.emptyNote = Docs.Create.TextDocument("", { - _width: 200, title: "text note", _autoHeight: true, system: true, - _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), - cloneFieldFilter: new List<string>(["system"]) - }); - ((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyImage === undefined) { - doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat", system: true }); - } - if (doc.emptyButton === undefined) { - doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button", system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyButton as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("http://www.bing.com/", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); - } - if (doc.emptyMap === undefined) { - doc.emptyMap = Docs.Create.MapDocument([], { title: "map", _showSidebar: true, _width: 800, _height: 600, system: true, cloneFieldFilter: new List<string>(["system"]) }); - ((doc.emptyMap as Doc).proto as Doc)["dragFactory-count"] = 0; - } - if (doc.activeMobileMenu === undefined) { - this.setupActiveMobileMenu(doc); - } - return [ - { toolTip: "Tap to create a note in a new pane, drag for a note", title: "Note", icon: "sticky-note", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyNote as Doc, noviceMode: true, clickFactory: doc.emptyNote as Doc, }, - { toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, }, - { toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, - { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc }, - { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc }, - { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, - { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc }, - { toolTip: "Tap to create a videoWall", title: "Wall", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWall as Doc }, - { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, - { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc }, - // { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, - { toolTip: "Tap to create a scripting box in a new pane, drag for a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScript as Doc }, - { toolTip: "Tap to create a mobile view in a new pane, drag for a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, - { toolTip: "Tap to create a custom header note document, drag for a custom header note", title: "Custom", icon: "window-maximize", click: 'openOnRight(delegateDragFactory(this.dragFactory))', drag: 'delegateDragFactory(this.dragFactory)', dragFactory: doc.emptyHeader as Doc }, - { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, - { toolTip: "Tap to create a map in the new pane, drag for a map", title: "Map", icon: "map-marker-alt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyMap as Doc, noviceMode: true } - ]; + const emptyThings:{key:string, // the field name where the empty thing will be stored + opts:DocumentOptions, // the document options that are required for the empty thing + funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing + creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist + }[] = [ + {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }}, + {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }}, + {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }}, + {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, + {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, + {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }}, + {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, + {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }}, + {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }}, + {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, + {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }}, + {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}}, + {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }}, + {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }}, + {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, + treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, + allowOverlayDrop: true, treeViewType: TreeViewType.outline, + backgroundColor: "white", _xMargin: 0, _yMargin: 0, _singleLine: true + }, funcs: {title: 'self.text?.Text'}}, + ]; + emptyThings.forEach(thing => this.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs)); + + return [ + { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, }, + { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, }, + { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, }, + { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, }, + { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, }, + { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, }, + { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, }, + { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} }, + { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} }, + { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}}, + { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, }, + { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, }, + { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: {onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } }, + ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, })) } - // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools - static async setupCreatorButtons(doc: Doc) { - let alreadyCreatedButtons: string[] = []; - const dragCreatorSet = Cast(doc.myItemCreators, Doc, null); - if (dragCreatorSet) { - const dragCreators = Cast(dragCreatorSet.data, listSpec(Doc)); - if (dragCreators) { - const dragDocs = await Promise.all(Array.from(dragCreators)); - alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title)); - } - } - const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); - const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, backgroundColor, dragFactory, noviceMode, clickFactory }) => Docs.Create.FontIconDocument({ - _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, - icon, - title, - toolTip, - btnType: ButtonType.ToolButton, - ignoreClick, - _dropAction: "alias", - onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, - onClick: click ? ScriptField.MakeScript(click) : undefined, - backgroundColor: backgroundColor ? backgroundColor : Colors.DARK_GRAY, - color: Colors.WHITE, - _hideContextMenu: true, - _removeDropProperties: new List<string>(["_stayInCollection"]), - _stayInCollection: true, - dragFactory, - clickFactory, - hidden: !noviceMode ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, - system: true, - })); + /// Initalizes the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools + static setupCreatorButtons(doc: Doc, dragCreatorDoc?:Doc):Doc { + const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { + const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; + const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, + _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, _dropAction: "alias", + btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true, + _removeDropProperties: new List<string>(["_stayInCollection"]), + }; + return this.AssignScripts(this.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); + }); - if (dragCreatorSet === undefined) { - doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { - title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true - })); - } else { - creatorBtns.forEach(nb => Doc.AddDocToList(doc.myItemCreators as Doc, "data", nb)); - } - return doc.myItemCreators as Doc; + const reqdOpts:DocumentOptions = { + title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true, + _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, + childDocumentsActive: true + }; + const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; + return this.AssignScripts(this.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); } - static async menuBtnDescriptions(doc: Doc) { + /// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents + static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] { + const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())"; return [ - { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, - { title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' }, - { title: "Files", target: Cast(doc.myFilesystem, Doc, null), icon: "folder-open", click: 'selectMainMenu(self)' }, - { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" }, - { title: "Imports", target: Cast(doc.myImportDocs, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, - { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, - { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, - { title: "Trails", target: Cast(doc.myTrails, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, - { title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" }, - ]; + { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", }, + { title: "Search", target: this.setupSearcher(doc, "mySearcher"), icon: "search", }, + { title: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", }, + { title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} }, + { title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, + { title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", }, + { title: "Shared Docs", target: this.MySharedDocs, icon: "users", funcs:{badgeValue:badgeValue}}, + { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", }, + { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, + ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}})); } - static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { - if (doc.menuStack === undefined) { - await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing - const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments, hidden }) => - Docs.Create.FontIconDocument({ - icon, - btnType: ButtonType.MenuButton, - _stayInCollection: true, - _hideContextMenu: true, - _chromeHidden: true, - system: true, - dontUndo: true, - title, - target, - hidden: hidden ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, - _dropAction: "alias", - _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), - _width: 60, - _height: 60, - watchedDocuments, - onClick: ScriptField.MakeScript(click, { scriptContext: "any" }) - }) - ); - - menuBtns.forEach(menuBtn => { - if (menuBtn.title === "Search") { - this.searchBtn = menuBtn; - } - }); - - menuBtns.forEach(menuBtn => { - if (menuBtn.title === "Search") { - doc.searchBtn = menuBtn; - } - }); - - doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { - title: "menuItemPanel", - childDropAction: "alias", - _chromeHidden: true, - backgroundColor: Colors.DARK_GRAY, - boxShadow: "rgba(0,0,0,0)", - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - ignoreClick: true, - _gridGap: 0, - _yMargin: 0, - _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true - })); - } - // this resets all sidebar buttons to being deactivated - PromiseValue(Cast(doc.menuStack, Doc)).then(stack => { - stack && PromiseValue(stack.data).then(btns => { - DocListCastAsync(btns).then(bts => bts?.forEach(btn => { - btn.dontUndo = true; - btn.system = true; - if (btn.title === "Catalog" || btn.title === "My Files") { // migration from Catalog to My Files - btn.target = Doc.UserDoc().myFilesystem; - btn.title = "My Files"; - } - })); - }); - }); - return doc.menuStack as Doc; + /// the empty panel that is filled with whichever left menu button's panel has been selected + static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { + this.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true}); } + /// Initializes the left sidebar menu buttons and the panels they open up + static setupLeftSidebarMenu(doc: Doc, field="myLeftSidebarMenu") { + this.setupLeftSidebarPanel(doc); + const myLeftSidebarMenu = DocCast(doc[field]); + const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => { + const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; + const reqdBtnOpts:DocumentOptions = { + title, icon, target, btnType: ButtonType.MenuButton, system: true, dontUndo: true, dontRegisterView: true, + _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _dropAction: "alias", + _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + }; + return this.AssignScripts(this.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); + }); - // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu - static setupActiveMobileMenu(doc: Doc) { - if (doc.activeMobileMenu === undefined) { - doc.activeMobileMenu = this.setupMobileMenu(); - } - return doc.activeMobileMenu as Doc; + const reqdStackOpts:DocumentOptions ={ + title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true + }; + return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); } - // Sets up mobileMenu stacking document - static setupMobileMenu() { - const menu = new PrefetchProxy(Docs.Create.StackingDocument(this.setupMobileButtons(), { - _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true, - })); - return menu; + // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu + static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") { + const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,}; + this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts); } - // SEts up mobile buttons for inside mobile menu + // Sets up mobile buttons for inside mobile menu static setupMobileButtons(doc?: Doc, buttons?: string[]) { + return []; const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [ { title: "DASHBOARDS", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Dashboards from your mobile, and navigate through all of your documents. " }, { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." }, @@ -723,483 +490,372 @@ export class CurrentUserUtils { }); } - static setupLibrary(userDoc: Doc) { - return CurrentUserUtils.setupDashboards(userDoc); + /// Search option on the left side button panel + static setupSearcher(doc: Doc, field:string) { + return this.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", system: true, childDropAction: "alias", + _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true, }); } - // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. - // when clicked, this panel will be displayed in the target container (ie, sidebarContainer) - static async setupToolsBtnPanel(doc: Doc) { - // setup a masonry view of all he creators - const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); - const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); - - doc["tabs-button-tools"] = undefined; - - if (doc.myCreators === undefined) { - doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { - title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, _fitWidth: true, - _width: 500, _height: 300, ignoreClick: true, _lockedPosition: true, system: true, _chromeHidden: true, - })); - } - // setup a color picker - if (doc.myColorPicker === undefined) { - const color = Docs.Create.ColorDocument({ - title: "color picker", _width: 220, _dropAction: "alias", _hideContextMenu: true, _stayInCollection: true, _forceActive: true, _removeDropProperties: new List<string>(["dropAction", "_stayInCollection", "_hideContextMenu", "forceActive"]), system: true - }); - doc.myColorPicker = new PrefetchProxy(color); - } - - if (doc.myTools === undefined) { - const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc], { - title: "My Tools", _showTitle: "title", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true, - system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, boxShadow: "0 0", - })) as any as Doc; - - doc.myTools = toolsStack; - } - } - - static async setupDashboards(doc: Doc) { - // setup dashboards library item - await doc.myDashboards; - if (doc.myDashboards === undefined) { - const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); - const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); - doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add", - treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true, - explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." - })); - const toggleDarkTheme = ScriptField.MakeScript(`this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`); - const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); - const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`); - const removeDashboard = ScriptField.MakeScript('removeDashboard(self)'); - const developerFilter = ScriptField.MakeFunction('!IsNoviceMode()'); - // (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, shareDashboard!, removeDashboard!]); - // (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Share Dashboard", "Remove Dashboard"]); - // (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "user-friends", "times"]); - (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleDarkTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]); - (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]); - (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "chalkboard", "tv", "camera", "users", "times"]); - (doc.myDashboards as any as Doc).childContextMenuFilters = new List<ScriptField>([undefined as any, developerFilter, developerFilter, developerFilter, undefined as any, undefined as any]); - } - return doc.myDashboards as any as Doc; - } - - static async setupPresentations(doc: Doc) { - await doc.myTrails; - if (doc.myTrails === undefined) { - const newTrail = ScriptField.MakeScript(`createNewPresentation()`); - const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); - doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "My Trails", _showTitle: "title", _height: 100, - treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, - explainer: "All of the trails that you have created will appear here." - })); - (doc.myTrails as any as Doc).contextMenuScripts = new List<ScriptField>([newTrail!]); - (doc.myTrails as any as Doc).contextMenuLabels = new List<string>(["Create New Trail"]); - (doc.myTrails as any as Doc).childContextMenuIcons = new List<string>(["plus"]); - } - return doc.myTrails as any as Doc; + /// Initializes the panel of draggable tools that is opened from the left sidebar. + 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 reqdToolOps:DocumentOptions = { + title: "My Tools", system: true, ignoreClick: true, boxShadow: "0 0", + _showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, + }; + return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); } - static async setupFilesystem(doc: Doc) { - await doc.myFilesystem; - if (doc.myFilesystem === undefined) { - doc.myFileOrphans = Docs.Create.TreeDocument([], { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); - // doc.myFileRoot = Docs.Create.TreeDocument([], { title: "file root", _stayInCollection: true, system: true, isFolder: true }); - const newFolder = ScriptField.MakeFunction(`makeTopLevelFolder()`, { scriptContext: "any" })!; - const newFolderButton: Doc = Docs.Create.FontIconDocument({ - onClick: newFolder, _forceActive: true, toolTip: "Create new folder", - _stayInCollection: true, _hideContextMenu: true, title: "New folder", btnType: ButtonType.ClickButton, _width: 30, _height: 30, - buttonText: "New folder", icon: "folder-plus", system: true - }); - doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], { - title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100, - treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, ignoreClick: true, - isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true, - explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." - })); - (doc.myFilesystem as any as Doc).contextMenuScripts = new List<ScriptField>([newFolder]); - (doc.myFilesystem as any as Doc).contextMenuLabels = new List<string>(["Create new folder"]); - (doc.myFilesystem as any as Doc).childContextMenuIcons = new List<string>(["plus"]); + /// initializes the left sidebar dashboard pane + static setupDashboards(doc: Doc, field:string) { + var myDashboards = DocCast(doc[field]); + + const newDashboard = `createNewDashboard()`; + const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, + title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", system: true }; + const reqdBtnScript = {onClick: newDashboard,} + const newDashboardButton = this.AssignScripts(this.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); + + const reqdOpts:DocumentOptions = { + title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true, + targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true, treeViewTruncateTitleWidth: 150, ignoreClick: true, + buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias", + _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, + contextMenuLabels: new List<string>(["Create New Dashboard"]), + contextMenuIcons: new List<string>(["plus"]), + childContextMenuLabels: new List<string>(["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]),// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters + childContextMenuIcons: new List<string>(["chalkboard", "tv", "camera", "users", "times"]), // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters + explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." + }; + myDashboards = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`; + const contextMenuScripts = [newDashboard]; + const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters + const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts + if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) { + myDashboards.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); + } + if (Cast(myDashboards.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) { + myDashboards.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); + } + if (Cast(myDashboards.childContextMenuFilters, listSpec(ScriptField), null)?.length !== childContextMenuFilters.length) { + myDashboards.childContextMenuFilters = new List<ScriptField>(childContextMenuFilters.map(script => !script ? script: ScriptField.MakeFunction(script)!)); } - return doc.myFilesystem as any as Doc; + return myDashboards; } - static setupRecentlyClosedDocs(doc: Doc) { - if (doc.myRecentlyClosedDocs === undefined) { - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); - const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true }); - doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true, - treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, ignoreClick: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, - explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." - - })); - (doc.myRecentlyClosedDocs as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Empty recently closed"]); - (doc.myRecentlyClosedDocs as any as Doc).contextMenuIcons = new List<string>(["trash"]); - + /// initializes the left sidebar Trails pane + static setupTrails(doc: Doc, field:string) { + var myTrails = DocCast(doc[field]); + const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, + title: "New trail", toolTip: "Create new trail", btnType: ButtonType.ClickButton, buttonText: "New trail", icon: "plus", system: true }; + const reqdBtnScript = {onClick: `createNewPresentation()`}; + const newTrailButton = this.AssignScripts(this.AssignOpts(DocCast(myTrails?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); + + const reqdOpts:DocumentOptions = { + title: "My Trails", _showTitle: "title", _height: 100, + treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton, + contextMenuIcons: new List<string>(["plus"]), + contextMenuLabels: new List<string>(["Create New Trail"]), + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, + explainer: "All of the trails that you have created will appear here." + }; + myTrails = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + const contextMenuScripts = [reqdBtnScript.onClick]; + if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) { + myTrails.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); } + return myTrails; } - static setupFilterDocs(doc: Doc) { - // setup Filter item - if (doc.currentFilter === undefined) { - doc.currentFilter = Docs.Create.FilterDocument({ - title: "Unnamed Filter", _height: 150, - treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none", - treeViewTruncateTitleWidth: 150, ignoreClick: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true - }); - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); - (doc.currentFilter as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.currentFilter as Doc).contextMenuLabels = new List<string>(["Clear All"]); - (doc.currentFilter as Doc).filterBoolean = "AND"; + /// initializes the left sidebar File system pane + static setupFilesystem(doc: Doc, field:string) { + var myFilesystem = DocCast(doc[field]); + const myFileOrphans = this.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); + + const newFolder = `makeTopLevelFolder()`; + const newFolderOpts: DocumentOptions = { + _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _width: 30, _height: 30, + title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", system: true + }; + const newFolderScript = { onClick: newFolder}; + const newFolderButton = this.AssignScripts(this.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); + + const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, + title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true, + isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true, + treeViewTruncateTitleWidth: 150, ignoreClick: true, childDropAction: "alias", + childContextMenuLabels: new List<string>(["Create new folder"]), + childContextMenuIcons: new List<string>(["plus"]), + explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." + }; + myFilesystem = this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]); + const childContextMenuScripts = [newFolder]; + if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) { + myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); } + return myFilesystem; } - static setupUserDoc(doc: Doc) { - if (doc.myUserDoc === undefined) { - doc.treeViewOpen = true; - doc.treeViewExpandedView = "fields"; - doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], { - treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title", - treeViewTruncateTitleWidth: 150, ignoreClick: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true - })) as any as Doc; + /// initializes the panel displaying docs that have been recently closed + static setupRecentlyClosed(doc: Doc, field:string) { + const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, + title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true, + treeViewTruncateTitleWidth: 150, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", + contextMenuLabels: new List<string>(["Empty recently closed"]), + contextMenuIcons:new List<string>(["trash"]), + explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." + }; + const recentlyClosed = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); + + const clearAll = (target:string) => `getProto(${target}).data = new List([])`; + const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, + title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", system: true, + toolTip: "Empty recently closed",}; + const clearDocsButton = this.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); + + if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton; + + if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) { + recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!]) } + return recentlyClosed; } - static setupSidebarContainer(doc: Doc) { - if (doc.sidebar === undefined) { - const sidebarContainer = new Doc(); - sidebarContainer.system = true; - doc.sidebar = new PrefetchProxy(sidebarContainer); - } - return doc.sidebar as Doc; + /// creates a new, empty filter doc + static createFilterDoc() { + const clearAll = `getProto(self).data = new List([])`; + const reqdOpts:DocumentOptions = { + _lockedPosition: true, _autoHeight: true, _fitWidth: true, _height: 150, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, + title: "Unnamed Filter", filterBoolean: "AND", boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, + childDropAction: "none", treeViewHideTitle: true, treeViewTruncateTitleWidth: 150, + childContextMenuLabels: new List<string>(["Clear All"]), + childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]), + }; + return Docs.Create.FilterDocument(reqdOpts); } - // setup the list of sidebar mode buttons which determine what is displayed in the sidebar - static async setupSidebarButtons(doc: Doc) { - CurrentUserUtils.setupSidebarContainer(doc); - await CurrentUserUtils.setupToolsBtnPanel(doc); - CurrentUserUtils.setupImportSidebar(doc); - CurrentUserUtils.setupDashboards(doc); - CurrentUserUtils.setupPresentations(doc); - CurrentUserUtils.setupFilesystem(doc); - CurrentUserUtils.setupRecentlyClosedDocs(doc); - CurrentUserUtils.setupUserDoc(doc); + /// initializes the left sidebar panel view of the UserDoc + static setupUserDocView(doc: Doc, field:string) { + const reqdOpts:DocumentOptions = { + _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", + boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, + treeViewHideTitle: true, treeViewTruncateTitleWidth: 150 + }; + if (!doc[field]) this.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); + return this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]); } - static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { + static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, { ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _lockedPosition: true, system: true, flexDirection: "row" - })) as any as Doc - - static createToolButton = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _removeDropProperties: new List<string>(["_dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true - })) as any as Doc - - /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window - static setupDockedButtons(doc: Doc) { - if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.createToolButton({ onClick: ScriptField.MakeScript("undo()"), _width: 30, _height: 30, dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to undo", title: "undo", icon: "undo-alt", system: true }); - } - if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.createToolButton({ onClick: ScriptField.MakeScript("redo()"), _width: 30, _height: 30, dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to redo", title: "redo", icon: "redo-alt", system: true }); - } - if (doc.dockedBtns === undefined) { - doc.dockedBtns = CurrentUserUtils.linearButtonList({ title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); - } - (doc["dockedBtn-undo"] as Doc).dontUndo = true; - (doc["dockedBtn-redo"] as Doc).dontUndo = true; + }) + + static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ + btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true, + _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]), + _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts, + }) + + /// initializes the required buttons in the expanding button menu at the bottom of the Dash window + static setupDockedButtons(doc: Doc, field="myDockedBtns") { + const dockedBtns = DocCast(doc[field]); + const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}) => + this.AssignScripts(this.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ?? + CurrentUserUtils.createToolButton(opts), scripts); + + const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet + { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, + { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }} + ]; + const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); + const dockBtnsReqdOpts = { + title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, + childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true + }; + reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + return this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } - static textTools(doc: Doc) { - const tools: Button[] = - [ - { - title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, - list: ["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", - "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"], - script: 'setFont(value, _readOnly_)' - }, - { title: "Font size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, script: 'setFontSize(value, _readOnly_)' }, - { title: "Font color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, script: 'setFontColor(value, _readOnly_)' }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", click: 'toggleBold(_readOnly_)' }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", click: 'toggleItalic(_readOnly_)' }, - { title: "Underline", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", click: 'toggleUnderline(_readOnly_)' }, - { title: "Bullet List", toolTip: "Bullet", btnType: ButtonType.ToggleButton, icon: "list", click: 'setBulletList("bullet", _readOnly_)' }, - { title: "Number List", toolTip: "Number", btnType: ButtonType.ToggleButton, icon: "list-ol", click: 'setBulletList("decimal", _readOnly_)' }, - - // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", click: 'toggleStrikethrough()'}, - // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", click: 'toggleSuperscript()'}, - // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", click: 'toggleSubscript()'}, - { title: "Left align", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", click: 'setAlignment("left", _readOnly_)' }, - { title: "Center align", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", click: 'setAlignment("center", _readOnly_)' }, - { title: "Right align", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", click: 'setAlignment("right", _readOnly_)' }, - ]; - return tools; + static textTools():Button[] { + return [ + { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'}, + btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, + { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, + { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_); }'}}, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} }, + { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} }, + { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} }, + { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} }, + + // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, + // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, + // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, + { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }}, + { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} }, + { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} }, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}}, + ]; } - static inkTools(doc: Doc) { - const tools: Button[] = [ - { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen", _readOnly_)' }, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", click: 'setActiveInkTool("eraser", _readOnly_)' }, - // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")' }, - { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle", _readOnly_)' }, - // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")' }, - { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", click: 'setActiveInkTool("line", _readOnly_)' }, - { title: "Fill color", toolTip: "Fill color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", script: "setFillColor(value, _readOnly_)" }, - { title: "Stroke width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, numBtnMin: 1, ignoreClick: true, script: 'setStrokeWidth(value, _readOnly_)' }, - { title: "Stroke color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, script: 'setStrokeColor(value, _readOnly_)' }, + static inkTools():Button[] { + return [ + { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts: {onClick:'{ return setActiveTool("pen", _readOnly_);}' }}, + { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }}, + // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} }, + { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} }, + // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' }, + { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick: '{ return setActiveTool("line", _readOnly_);}' }}, + { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: "{ return setFillColor(value, _readOnly_);}"} }, + { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1}, + { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} }, ]; - return tools; } - static schemaTools(doc: Doc) { - const tools: Button[] = - [ - { - title: "Show preview", - toolTip: "Show preview of selected document", - btnType: ButtonType.ToggleButton, - buttonText: "Show Preview", - icon: "eye", - click: 'toggleSchemaPreview(_readOnly_)', - }, - ]; - return tools; + static schemaTools():Button[] { + return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }]; } - static webTools(doc: Doc) { - const tools: Button[] = - [ - { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack(_readOnly_)' }, - { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward(_readOnly_)' }, - //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' }, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL(value, _readOnly_)' }, - ]; - - return tools; + static webTools() { + return [ + { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }}, + { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}}, + //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, + ]; } - static async contextMenuTools(doc: Doc) { + static contextMenuTools():Button[] { return [ - { - title: "Perspective", toolTip: "View", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, - list: [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree, - CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, - CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, - CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, - CollectionViewType.Grid, CollectionViewType.NoteTaking], - script: 'setView(value, _readOnly_)', - }, // Always show - { - title: "Background Color", toolTip: "Background Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", - script: "setBackgroundColor(value, _readOnly_)", hidden: 'selectedDocumentType()' - }, // Only when a document is selected - { - title: "Header Color", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "heading", - script: "setHeaderColor(value, _readOnly_)", hidden: 'selectedDocumentType()', - }, // Only when a document is selected - { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", click: 'toggleOverlay(_readOnly_)', hidden: 'selectedDocumentType(undefined, "freeform", true)' }, // Only when floating document is selected in freeform - // { title: "Alias", btnType: ButtonType.ClickButton, icon: "copy", hidden: 'selectedDocumentType()' }, // Only when a document is selected - { title: "Text", type: "textTools", subMenu: true, expanded: 'selectedDocumentType("rtf")' }, // Always available - { title: "Ink", type: "inkTools", subMenu: true, expanded: 'selectedDocumentType("ink")' }, // Always available - { title: "Web", type: "webTools", subMenu: true, hidden: 'selectedDocumentType("web")' }, // Only when Web is selected - { title: "Schema", type: "schemaTools", subMenu: true, hidden: 'selectedDocumentType(undefined, "schema")' } // Only when Schema is selected + { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree, + CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, + CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, + CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, + CollectionViewType.Grid]), + title: "Perspective", toolTip: "View", width: 100,btnType: ButtonType.DropdownList,ignoreClick: true, scripts: { script: 'setView(value, _readOnly_)'}}, + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",width: 20, btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setBackgroundColor(value, _readOnly_)'},funcs: {hidden: '!selectedDocumentType()'}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}, funcs: {hidden: '!selectedDocumentType()'}}, + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, scripts: { onClick: 'toggleOverlay(_readOnly_)'}, funcs: {hidden: '!selectedDocumentType(undefined, "freeform", true)'}}, // Only when floating document is selected in freeform + { title: "Text", icon: "text", subMenu: CurrentUserUtils.textTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.RTF}")`} }, // Always available + { title: "Ink", icon: "ink", subMenu: CurrentUserUtils.inkTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.INK}")`} }, // Always available + { title: "Web", icon: "web", subMenu: CurrentUserUtils.webTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.WEB}")`, hidden: `!selectedDocumentType("${DocumentType.WEB}")`} }, // Only when Web is selected + { title: "Schema", icon: "schema", subMenu: CurrentUserUtils.schemaTools(), funcs: {linearViewIsExpanded: `selectedDocumentType(undefined, "${CollectionViewType.Schema}")`, hidden: `!selectedDocumentType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected ]; } - // Sets up the default context menu buttons - static async setupContextMenuButtons(doc: Doc) { - if (doc.contextMenuBtns === undefined) { - const docList: Doc[] = []; - - (await CurrentUserUtils.contextMenuTools(doc)).map(({ title, width, list, toolTip, ignoreClick, icon, type, btnType, click, script, subMenu, hidden, expanded }) => { - const menuDocList: Doc[] = []; - if (subMenu) { - // default is textTools - let tools: Button[]; - switch (type) { - case "inkTools": - tools = CurrentUserUtils.inkTools(doc); - break; - case "schemaTools": - tools = CurrentUserUtils.schemaTools(doc); - break; - case "webTools": - tools = CurrentUserUtils.webTools(doc); - break; - case "textTools": - tools = CurrentUserUtils.textTools(doc); - break; - default: - tools = CurrentUserUtils.textTools(doc); - break; - } - tools.map(({ title, toolTip, icon, btnType, numBtnType, numBtnMax, numBtnMin, click, script, width, list, ignoreClick, switchToggle }) => { - const computed = click ? ComputedField.MakeFunction(click) as any : "transparent"; - menuDocList.push(Docs.Create.FontIconDocument({ - _nativeWidth: width ? width : 25, - _nativeHeight: 25, - _width: width ? width : 25, - _height: 25, - icon, - toolTip, - numBtnType, - numBtnMin, - numBtnMax, - script: script ? ScriptField.MakeScript(script, { value: "any" }) : undefined, - btnType: btnType, - btnList: new List<string>(list), - ignoreClick: ignoreClick, - _stayInCollection: true, - _hideContextMenu: true, - _lockedPosition: true, - system: true, - dontUndo: true, - title, - switchToggle, - color: Colors.WHITE, - backgroundColor: computed, - _dropAction: "alias", - _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), - onClick: click ? ScriptField.MakeScript(click) : undefined - })); - }); - docList.push(CurrentUserUtils.linearButtonList({ - linearViewSubMenu: true, - flexGap: 0, - ignoreClick: true, - linearViewExpandable: true, - icon: title, - _height: 30, - // backgroundColor: hidden ? ComputedField.MakeFunction(hidden, { }, { _readOnly_: true }) as any : "transparent", - linearViewIsExpanded: expanded ? !(ComputedField.MakeFunction(expanded) as any) : undefined, - hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, - }, menuDocList)); - } else { - docList.push(Docs.Create.FontIconDocument({ - _nativeWidth: width ? width : 25, - _nativeHeight: 25, - _width: width ? width : 25, - _height: 25, - icon, - toolTip, - script: script ? ScriptField.MakeScript(script, { value: "any" }) : undefined, - btnType, - btnList: new List<string>(list), - ignoreClick, - _stayInCollection: true, - _hideContextMenu: true, - _lockedPosition: true, - system: true, - dontUndo: true, - title, - color: Colors.WHITE, - // backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult, {}, {_readOnly_:true}) as any : "transparent", - _dropAction: "alias", - hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, - _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), - onClick: click ? ScriptField.MakeScript(click, { scriptContext: "any" }, { _readOnly_: false }) : undefined - })); - } - }); - - doc.contextMenuBtns = CurrentUserUtils.linearButtonList({ title: "menu buttons", flexGap: 0, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }, docList); + /// initializes a context menu button for the top bar context menu + static setupContextMenuButton(params:Button, btnDoc?:Doc) { + const reqdOpts:DocumentOptions = { + ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, + backgroundColor: params.scripts?.onClick ? undefined: "transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background + color: Colors.WHITE, system: true, dontUndo: true, + _nativeWidth: params.width ?? 30, _width: params.width ?? 30, + _height: 30, _nativeHeight: 30, + _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, + _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + }; + const reqdFuncs:{[key:string]:any} = { + ...params.funcs, + backgroundColor: params.scripts?.onClick /// a bit hacky. if onClick is set, then we assume it returns a color value when queried with '_readOnly_'. This will be true for toggle buttons, but not generally } + return this.AssignScripts(this.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs); } - // sets up the default set of documents to be shown in the Overlay layer - static setupOverlays(doc: Doc) { - if (doc.myOverlayDocs === undefined) { - doc.myOverlayDocs = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6", system: true })); - } + /// Initializes all the default buttons for the top bar context menu + static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { + const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }; + const ctxtMenuBtnsDoc = this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); + const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => { + const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title); + if (!params.subMenu) { + return this.setupContextMenuButton(params, menuBtnDoc); + } else { + const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, + childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true, + linearViewSubMenu: true, linearViewExpandable: true, }; + const items = params.subMenu?.map(sub => + this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title)) + ); + return this.AssignScripts( + this.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs); + } + }); + return this.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } - // the initial presentation Doc to use - static setupDefaultPresentation(doc: Doc) { - if (doc["template-presentation"] === undefined) { - doc["template-presentation"] = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ - title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _fitWidth: true, _height: 46, isTemplateDoc: true, isTemplateForField: "data", system: true - })); - } + /// collection of documents rendered in the overlay layer above all tabs and other UI + static setupOverlays(doc: Doc, field = "myOverlayDocs") { + return this.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true }); } - // Sharing sidebar is where shared documents are contained - static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { - if (doc.myLinkDatabase === undefined) { - let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId); - if (!linkDocs) { - linkDocs = new Doc(linkDatabaseId, true); - (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail; - (linkDocs as Doc).author = Doc.CurrentUserEmail; - (linkDocs as Doc).data = new List<Doc>([]); - (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment; - } + static setupPublished(doc:Doc, field = "myPublishedDocs") { + return this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true }); + } + + /// The database of all links on all documents + static setupLinkDocs(doc: Doc, linkDatabaseId: string) { + if (!(Docs.newAccount ? undefined : DocCast(doc.myLinkDatabase))) { + const linkDocs = new Doc(linkDatabaseId, true); + linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail; + linkDocs.author = Doc.CurrentUserEmail; + linkDocs.data = new List<Doc>([]); + linkDocs["acl-Public"] = SharingPermissions.Augment; doc.myLinkDatabase = new PrefetchProxy(linkDocs); } - // TODO:glr NOTE: treeViewHideTitle & _showTitle may be confusing, treeViewHideTitle is for the editable title (just for tree view), _showTitle is to show the Document title for any document - if (doc.mySharedDocs === undefined) { - let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer"); - if (!sharedDocs) { - sharedDocs = Docs.Create.TreeDocument([], { - title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, - _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, - _chromeHidden: true, boxShadow: "0 0", - explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" - }, sharingDocumentId + "outer", sharingDocumentId); - (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment; - } - if (sharedDocs instanceof Doc) { - Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)"; - const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); - const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name }); - sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]); - sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]); - sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]); - sharedDocs.childContextMenuIcons = new List<string>(["user-plus",]); - - } - doc.mySharedDocs = new PrefetchProxy(sharedDocs); - } } - // Import sidebar is where shared documents are contained - static setupImportSidebar(doc: Doc) { - if (doc.myImportDocs === undefined) { - const newImportButton: Doc = Docs.Create.FontIconDocument({ onClick: ScriptField.MakeScript("importDocument()"), _forceActive: true, toolTip: "Import from computer", _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", system: true }); - doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { - title: "My Imports", _forceActive: true, buttonMenu: true, buttonMenuDoc: newImportButton, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, - childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, - explainer: "This is where documents that are Imported into Dash will go." - })); - } + /// Shared documents option on the left side button panel + // A user's sharing document is where all documents that are shared to that user are placed. + // When the user views one of these documents, it will be added to the sharing documents 'viewed' list field + // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents + static setupSharedDocs(doc: Doc, sharingDocumentId: string) { + const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); + const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name }); + const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}"; + + const sharedScripts = { treeViewChildDoubleClick: dblClkScript, } + const sharedDocOpts:DocumentOptions = { + title: "My Shared Docs", + userColor: "rgb(202, 202, 202)", + childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]), + childContextMenuScripts: new List<ScriptField>([addToDashboards!,]), + childContextMenuLabels: new List<string>(["Add to Dashboards",]), + childContextMenuIcons: new List<string>(["user-plus",]), + "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, + childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, + // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar + _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, + explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" + }; + + this.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); } - // Search sidebar is where searches within the document are performed - static setupSearchSidebar(doc: Doc) { - if (doc.mySearchPanel === undefined) { - doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({ - backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true, - childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true - })) as any as Doc; - } + /// Import option on the left side button panel + static setupImportSidebar(doc: Doc, field:string) { + const reqdOpts:DocumentOptions = { + title: "My Imports", _forceActive: true, buttonMenu: true, ignoreClick: true, _showTitle: "title", + _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, + childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, + dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go." + }; + const myImports = this.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts); + + const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer", + _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, + buttonText: "Import", icon: "upload", system: true }; + this.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); + return myImports; } static setupClickEditorTemplates(doc: Doc) { @@ -1212,9 +868,8 @@ export class CurrentUserUtils { 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 }); + 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 }); } @@ -1250,81 +905,80 @@ export class CurrentUserUtils { return doc.clickFuncs as Doc; } - static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { - if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); - const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); - reaction(() => DateCast((doc.globalGroupDatabase as Doc)["data-lastModified"]), + + /// 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. + static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + this.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); + reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]), async () => { - const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); }, { fireImmediately: true }); - // Document properties on load - doc.system = true; - doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; - doc.title = Doc.CurrentUserEmail; - doc._raiseWhenDragged = true; - doc._showLabel = false; - doc._showMenuLabel = true; - doc.textAlign = StrCast(doc.textAlign, "left"); - doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); - doc.activeInkWidth = Number(StrCast(doc.activeInkWidth, "1")); - doc.activeInkBezier = StrCast(doc.activeInkBezier, "0"); - doc.activeFillColor = StrCast(doc.activeFillColor, ""); - doc.activeArrowStart = StrCast(doc.activeArrowStart, ""); - doc.activeArrowEnd = StrCast(doc.activeArrowEnd, ""); - doc.activeDash = StrCast(doc.activeDash, "0"); - doc.fontSize = StrCast(doc.fontSize, "12px"); - doc.fontFamily = StrCast(doc.fontFamily, "Arial"); - doc.fontColor = StrCast(doc.fontColor, "black"); - doc.fontHighlight = StrCast(doc.fontHighlight, ""); - doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false); - doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null); - doc.noviceMode = BoolCast(doc.noviceMode, true); - doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // - doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // - Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); - doc.savedFilters = new List<Doc>(); + doc.system ?? (doc.system = true); + doc.title ?? (doc.title = Doc.CurrentUserEmail); + Doc.noviceMode ?? (Doc.noviceMode = true); + doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true); + doc._showLabel ?? (doc._showLabel = true); + doc.textAlign ?? (doc.textAlign = "left"); + doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");; + doc.activeInkWidth ?? (doc.activeInkWidth = 1); + doc.activeInkBezier ?? (doc.activeInkBezier = "0"); + doc.activeFillColor ?? (doc.activeFillColor = ""); + doc.activeArrowStart ?? (doc.activeArrowStart = ""); + doc.activeArrowEnd ?? (doc.activeArrowEnd = ""); + doc.activeDash ?? (doc.activeDash == "0"); + doc.fontSize ?? (doc.fontSize = "12px"); + doc.fontFamily ?? (doc.fontFamily = "Arial"); + doc.fontColor ?? (doc.fontColor = "black"); + doc.fontHighlight ?? (doc.fontHighlight = ""); + doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false); + doc.savedFilters ?? (doc.savedFilters = new List<Doc>()); doc.filterDocCount = 0; doc.freezeChildren = "remove|add"; + this.setupLinkDocs(doc, linkDatabaseId); + this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon - this.setupDocTemplates(doc); // sets up the template menu of templates - this.setupImportSidebar(doc); // sets up the import sidebar - this.setupSearchSidebar(doc); // sets up the search sidebar this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile - this.setupOverlays(doc); // documents in overlay layer - this.setupContextMenuButtons(doc); // set up context menu buttons + this.setupOverlays(doc); // sets up the overlay panel where documents and other widgets can be added to float over the rest of the dashboard + this.setupPublished(doc); // sets up the list doc of all docs that have been published (meaning that they can be auto-linked by typing their title into another text box) + this.setupContextMenuButtons(doc); // set up the row of buttons at the top of the dashboard that change depending on what is selected this.setupDockedButtons(doc); // the bottom bar of font icons - await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels - await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId); - if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - - setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered - - // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet - doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); - doc["dockedBtn-redo"] && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc["dockedBtn-redo"] as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); - - // uncomment this to setup a default note style that uses the custom header layout - // PromiseValue(doc.emptyHeader).then(factory => { - // if (Cast(doc.defaultTextLayout, Doc, null)?.version !== headerViewVersion) { - // const deleg = Doc.delegateDragFactory(factory as Doc); - // deleg.title = "header"; - // doc.defaultTextLayout = new PrefetchProxy(deleg); - // Doc.AddDocToList(Cast(doc["template-notes"], Doc, null), "data", deleg); - // } - // }); - setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500); - doc.fieldInfos = await Docs.setupFieldInfos(); + this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left + this.setupDocTemplates(doc); // sets up the template menu of templates + this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption + this.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); + this.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents + if (doc.activeDashboard instanceof Doc) { // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme; } - if (doc.activeCollectionBackground === "white") { // temporary to avoid having to rebuild the databse for old accounts that have this set by default. - doc.activeCollectionBackground = undefined; - } + new LinkManager(); + + DocServer.UPDATE_SERVER_CACHE(); return doc; } + static setupFieldInfos(doc:Doc, field="fieldInfos") { + const fieldInfoOpts = { title: "Field Infos", system: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object + const infos = this.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts); + const entries = Object.entries(new DocumentOptions()); + entries.forEach(pair => { + if (!Array.from(Object.keys(fieldInfoOpts)).includes(pair[0])) { + const options = pair[1] as FInfo; + const opts:DocumentOptions = { system: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")}; + switch (options.fieldType) { + case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break; + case "number": opts.fieldValues = new List<number>(options.values as any); break; + case Doc.name: opts.fieldValues = new List<Doc>(options.values as any); break; + default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType + } + this.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts); + } + }); + } public static async loadCurrentUser() { return rp.get(Utils.prepend("/getCurrentUser")).then(async response => { @@ -1350,12 +1004,8 @@ export class CurrentUserUtils { Docs.newAccount = !(field instanceof Doc); await Docs.Prototypes.initialize(); const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc; - const updated = this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); - (await DocListCastAsync(Cast(Doc.UserDoc().myLinkDatabase, Doc, null)?.data))?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager - const a1 = await Cast(link?.anchor1, Doc, null); - const a2 = await Cast(link?.anchor2, Doc, null); - }); - return updated; + Docs.newAccount &&(userDoc.activePage = "home"); + return this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); }); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); @@ -1365,15 +1015,18 @@ export class CurrentUserUtils { public static _urlState: HistoryUtil.DocUrl; - public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => { + /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there). + /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) + public static openDashboard = (doc: Doc|undefined, fromHistory = false) => { + if (!doc) return false; CurrentUserUtils.MainDocId = doc[Id]; + Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc); - if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard - !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); - userDoc ? (userDoc.activeDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); - } + // this has the side-effect of setting the main container since we're assigning the active/guest dashboard + Doc.UserDoc() ? (CurrentUserUtils.ActiveDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); + const state = CurrentUserUtils._urlState; - if (state.sharing === true && !userDoc) { + if (state.sharing === true && !Doc.UserDoc()) { DocServer.Control.makeReadOnly(); } else { fromHistory || HistoryUtil.pushState({ @@ -1409,49 +1062,47 @@ export class CurrentUserUtils { input.onchange = async _e => { const upload = Utils.prepend("/uploadDoc"); const formData = new FormData(); - const file = input.files && input.files[0]; - if (file && file.type === 'application/zip') { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = Docs.newAccount ? undefined : await DocServer.GetRefField(json); - if (doc instanceof Doc) { - setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => - docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. - } - } + const file = input.files?.[0]; + if (file?.type === 'application/zip') { + const doc = await Doc.importDocument(file); + // NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) { + // setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => + // docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. + // } + const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null); + doc instanceof Doc && list?.splice(0, 0, doc); } else if (input.files && input.files.length !== 0) { - const importDocs = Cast(Doc.UserDoc().myImportDocs, Doc, null); const disposer = OverlayView.ShowSpinner(); - DocListCastAsync(importDocs.data).then(async list => { - const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {}); - if (results.length !== input.files?.length) { - alert("Error uploading files - possibly due to unsupported file types"); - } - list?.splice(0, 0, ...results); - disposer(); - }); + const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {}); + if (results.length !== input.files?.length) { + alert("Error uploading files - possibly due to unsupported file types"); + } + const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null); + list?.splice(0, 0, ...results); + disposer(); } else { console.log("No file selected"); } }; input.click(); } + + public static snapshotDashboard() { return CollectionDockingView.TakeSnapshot(CurrentUserUtils.ActiveDashboard); } - public static async snapshotDashboard(userDoc: Doc) { - const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard); - Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy); - CurrentUserUtils.openDashboard(userDoc, copy); - } + public static closeActiveDashboard = () => { CurrentUserUtils.ActiveDashboard = undefined; } - public static createNewDashboard = async (userDoc: Doc, id?: string) => { - const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); - const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc; + public static removeDashboard = async (dashboard:Doc) => { + const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data); + if (dashboards?.length) { + if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(dashboards.find(doc => doc !== dashboard)); + Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard); + if (!dashboards.length) CurrentUserUtils.ActivePage = "home"; + } + } + public static createNewDashboard = (id?: string, name?: string) => { + const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); + const dashboards = CurrentUserUtils.MyDashboards; const dashboardCount = DocListCast(dashboards.data).length + 1; - const emptyPane = Cast(userDoc.emptyPane, Doc, null); - emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, @@ -1459,40 +1110,30 @@ export class CurrentUserUtils { _height: 1000, _fitWidth: true, _backgroundGridShow: true, - title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + title: `Untitled Tab 1`, }; + const title = name ? name : `Dashboard ${dashboardCount}` const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); + const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, "row"); freeformDoc.context = dashboardDoc; // switching the tabs from the datadoc to the regular doc - const dashboardTabs = dashboardDoc[DataSym].data; - dashboardDoc[DataSym].data = new List<Doc>(); - dashboardDoc.data = dashboardTabs; - - // collating all docs on the dashboard to make a data-all field - const allDocs = new List<Doc>(); - const allDocs2 = new List<Doc>(); // Array.from, spread, splice all cause so stack or acl issues for some reason - DocListCast(dashboardTabs).forEach(doc => { - const tabDocs = DocListCast(doc.data); - allDocs.push(...tabDocs); - allDocs2.push(...tabDocs); - }); - dashboardDoc[DataSym]["data-all"] = allDocs; - dashboardDoc["data-all"] = allDocs2; - DocListCast(dashboardDoc.data).forEach(doc => doc.dashboard = dashboardDoc); - DocListCast(dashboardDoc.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any; + const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); + dashboardDoc.data = new List<Doc>(dashboardTabs); + dashboardDoc["pane-count"] = 1; - userDoc.activePresentation = presentation; + CurrentUserUtils.ActivePresentation = presentation; Doc.AddDocToList(dashboards, "data", dashboardDoc); - CurrentUserUtils.openDashboard(userDoc, dashboardDoc); + // open this new dashboard + CurrentUserUtils.ActiveDashboard = dashboardDoc; + CurrentUserUtils.ActivePage = "dashboard"; } public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) { const tbox = Docs.Create.TextDocument("", { _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, backgroundColor: backgroundColor, - _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, title + _width: width || 200, _height: 35, x: x, y: y, _fitWidth: true, _autoHeight: true, title }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { @@ -1503,105 +1144,52 @@ export class CurrentUserUtils { return tbox; } - public static get MySearchPanelDoc() { return Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null); } - public static get ActiveDashboard() { return Cast(Doc.UserDoc().activeDashboard, Doc, null); } - public static get ActivePresentation() { return Cast(Doc.UserDoc().activePresentation, Doc, null); } - public static get MyRecentlyClosed() { return Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc, null); } - public static get MyDashboards() { return Cast(Doc.UserDoc().myDashboards, Doc, null); } - public static get EmptyPane() { return Cast(Doc.UserDoc().emptyPane, Doc, null); } - public static get OverlayDocs() { return DocListCast((Doc.UserDoc().myOverlayDocs as Doc)?.data); } - public static set SelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; } - @computed public static get SelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } + public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } + public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } + public static get MySearcher() { return DocCast(Doc.UserDoc().mySearcher); } + public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } + public static get MyHeaderBar() { return DocCast(Doc.UserDoc().myHeaderBar); } + public static get MyTools() { return DocCast(Doc.UserDoc().myTools); } + public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } + public static get MyFileOrphans() { return DocCast(Doc.UserDoc().myFileOrphans); } + public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } + public static get MyLeftSidebarMenu() { return DocCast(Doc.UserDoc().myLeftSidebarMenu); } + public static get MyLeftSidebarPanel() { return DocCast(Doc.UserDoc().myLeftSidebarPanel); } + public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } + public static get MyTrails() { return DocCast(Doc.UserDoc().myTrails); } + public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } + public static get MyContextMenuBtns() { return DocCast(Doc.UserDoc().myContextMenuBtns); } + public static get MyRecentlyClosed() { return DocCast(Doc.UserDoc().myRecentlyClosed); } + public static get MyOverlayDocs() { return DocCast(Doc.UserDoc().myOverlayDocs); } + public static get MyPublishedDocs() { return DocCast(Doc.UserDoc().myPublishedDocs); } + public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } + public static set ActiveDashboard(val:Doc|undefined) { Doc.UserDoc().activeDashboard = val; } + public static get ActivePresentation() { return DocCast(Doc.UserDoc().activePresentation); } + public static set ActivePresentation(val) { Doc.UserDoc().activePresentation = val; } + public static get ActivePage() { return StrCast(Doc.UserDoc().activePage); } + public static set ActivePage(val) { Doc.UserDoc().activePage = val; } + public static set ActiveTool(tool: InkTool) { Doc.UserDoc().activeTool = tool; } + public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } } -ScriptingGlobals.add(function openDragFactory(dragFactory: Doc) { - const copy = Doc.copyDragFactory(dragFactory); - if (copy) { - CollectionDockingView.AddSplit(copy, "right"); - const view = DocumentManager.Instance.getFirstDocumentView(copy); - view && SelectionManager.SelectView(view, false); - } -}); -ScriptingGlobals.add(function MySharedDocs() { return Doc.SharingDoc(); }, - "document containing all shared Docs"); -ScriptingGlobals.add(function IsNoviceMode() { return Doc.UserDoc().noviceMode; }, - "is Dash in novice mode"); -ScriptingGlobals.add(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); }, - "creates a snapshot copy of a dashboard"); -ScriptingGlobals.add(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(Doc.UserDoc()); }, - "creates a new dashboard when called"); -ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, - "creates a new presentation when called"); -ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, - "creates a new folder in myFiles when called"); -ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, - "returns all the links to the document or its annotations", "(doc: any)"); -ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, - "imports files from device directly into the import sidebar"); -ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { - SharingManager.Instance.open(undefined, dashboard); -}, - "opens sharing dialog for Dashboard"); -ScriptingGlobals.add(async function removeDashboard(dashboard: Doc) { - const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data); - if (dashboards && dashboards.length > 1) { - if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboards.find(doc => doc !== dashboard)!); - Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard); - } -}, - "Remove Dashboard from Dashboards"); -ScriptingGlobals.add(async function addToDashboards(dashboard: Doc) { - const dashboardAlias = Doc.MakeAlias(dashboard); - - const allDocs = await DocListCastAsync(dashboard[DataSym]["data-all"]); - - // moves the data-all field from the datadoc to the layoutdoc, necessary for off screen docs tab to function properly - // dashboard["data-all"] = new List<Doc>(allDocs); - // dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc))); - - // const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig)); - // dashboardAlias.dockingConfig = JSON.stringify(dockingConfig); - - dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder))); - DocListCast(dashboardAlias.data).forEach(doc => doc.dashboard = dashboardAlias); - //new List<Doc>(); - DocListCast(dashboardAlias.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any; - Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias); - CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias); -}, - "adds Dashboard to set of Dashboards"); - -/** - * Dynamically computes which docs should be rendered in the off-screen tabs tree of a dashboard. - */ -ScriptingGlobals.add(function dynamicOffScreenDocs(dashboard: Doc) { - if (dashboard[DataSym] instanceof Doc) { - const allDocs = DocListCast(dashboard["data-all"]); - const onScreenTab = DocListCast(dashboard.data)[0]; - const onScreenDocs = DocListCast(onScreenTab.data); - return new List<Doc>(allDocs.reduce((result: Doc[], doc) => { - !onScreenDocs.includes(doc) && !onScreenDocs.includes(doc.aliasOf as Doc) && (result.push(doc)); - return result; - }, [])); - } - return []; -}); -ScriptingGlobals.add(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkParent?: boolean) { - let selected = SelectionManager.Docs().length ? SelectionManager.Docs()[0] : undefined; - if (selected && checkParent) { - const parentDoc: Doc = Cast(selected.context, Doc, null); - selected = parentDoc; - } - if (selected && docType && selected.type === docType) return false; - else if (selected && colType && selected.viewType === colType) return false; - else if (selected && !colType && !docType) return false; - else return true; +ScriptingGlobals.add(function MySharedDocs() { return CurrentUserUtils.MySharedDocs; }, "document containing all shared Docs"); +ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); +ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); +ScriptingGlobals.add(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(); }, "creates a snapshot copy of a dashboard"); +ScriptingGlobals.add(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(); }, "creates a new dashboard when called"); +ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called"); +ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called"); +ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)"); +ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); +ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { SharingManager.Instance.open(undefined, dashboard); }, "opens sharing dialog for Dashboard"); +ScriptingGlobals.add(function removeDashboard(dashboard: Doc) { CurrentUserUtils.removeDashboard(dashboard); }, "Remove Dashboard from Dashboards"); +ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { CurrentUserUtils.openDashboard( Doc.MakeAlias(dashboard)); }, "adds Dashboard to set of Dashboards"); +ScriptingGlobals.add(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { + let selected = (sel => checkContext ? DocCast(sel?.context) : sel)(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); + return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; }); ScriptingGlobals.add(function makeTopLevelFolder() { - const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true }); - TreeView._editTitleOnLoad = { id: folder[Id], parent: undefined }; - return Doc.AddDocToList(Doc.UserDoc().myFilesystem as Doc, "data", folder); -}); -ScriptingGlobals.add(function toggleComicMode() { - Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; + TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; + const opts = { title: "Untitled folder", _stayInCollection: true, isFolder: true }; + return Doc.AddDocToList(CurrentUserUtils.MyFilesystem, "data", Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id)); });
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 0a00ab6e0..0435cd535 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -5,17 +5,19 @@ import { Cast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; import { CollectionView } from '../views/collections/CollectionView'; import { LightboxView } from '../views/LightboxView'; import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; +import { CurrentUserUtils } from './CurrentUserUtils'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) - @observable public DocumentViews: DocumentView[] = []; + @observable public DocumentViews = new Set<DocumentView>(); @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; @@ -45,7 +47,7 @@ export class DocumentManager { // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { - this.DocumentViews.push(view); + this.DocumentViews.add(view); } } public RemoveView = action((view: DocumentView) => { @@ -60,8 +62,7 @@ export class DocumentManager { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { - const index = this.DocumentViews.indexOf(view); - index !== -1 && this.DocumentViews.splice(index, 1); + this.DocumentViews.delete(view); } SelectionManager.DeselectView(view); }); @@ -69,13 +70,13 @@ export class DocumentManager { //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; - DocumentManager.Instance.DocumentViews.map(view => { + Array.from(DocumentManager.Instance.DocumentViews).map(view => { if (view.rootDoc[Id] === id) { toReturn.push(view); } }); if (toReturn.length === 0) { - DocumentManager.Instance.DocumentViews.map(view => { + Array.from(DocumentManager.Instance.DocumentViews).map(view => { const doc = view.rootDoc.proto; if (doc && doc[Id] && doc[Id] === id) { toReturn.push(view); @@ -95,14 +96,14 @@ export class DocumentManager { const passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; for (const pass of passes) { - DocumentManager.Instance.DocumentViews.map(view => { + Array.from(DocumentManager.Instance.DocumentViews).map(view => { if (view.rootDoc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; return; } }); if (!toReturn) { - DocumentManager.Instance.DocumentViews.map(view => { + Array.from(DocumentManager.Instance.DocumentViews).map(view => { const doc = view.rootDoc.proto; if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; @@ -121,9 +122,8 @@ export class DocumentManager { } public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { - const docViews = DocumentManager.Instance.DocumentViews; const views: DocumentView[] = []; - docViews.map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); + Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); } public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { @@ -132,8 +132,8 @@ export class DocumentManager { } public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; - const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); - const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); + const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); + const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); // heuristic to return the "best" documents first: // choose a document in the lightbox first @@ -155,7 +155,7 @@ export class DocumentManager { targetDoc: Doc, // document to display willZoom: boolean, // whether to zoom doc to take up most of screen createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist - docContext?: Doc, // context to load that should contain the target + docContext: Doc[], // context to load that should contain the target linkDoc?: Doc, // link that's being followed closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc @@ -167,49 +167,78 @@ export class DocumentManager { originalTarget = originalTarget ?? targetDoc; const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; const docView = getFirstDocView(targetDoc, originatingDoc); - const wasHidden = targetDoc.hidden; // - if (wasHidden) runInAction(() => targetDoc.hidden = false); // if the target is hidden, un-hide it here. + const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); + const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? targetDoc : targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field + var wasHidden = resolvedTarget.hidden; + if (wasHidden) { + runInAction(() => { + resolvedTarget.hidden = false; // if the target is hidden, un-hide it here. + docView?.props.bringToFront(resolvedTarget); + }); + } const focusAndFinish = (didFocus: boolean) => { + const finalTargetDoc = resolvedTarget; if (originatingDoc?.isPushpin) { if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal - targetDoc.hidden = !targetDoc.hidden; + finalTargetDoc.hidden = !finalTargetDoc.hidden; } } else { - targetDoc.hidden && (targetDoc.hidden = undefined); + finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined); !noSelect && docView?.select(false); } finished?.(); - return false; }; - const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); - const annoContainerView = annotatedDoc && getFirstDocView(annotatedDoc); - const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; - const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined; + const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc); + const contextDocs = docContext.length ? await DocListCastAsync(docContext[0].data) : undefined; + const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined; const targetDocContext = contextDoc || annotatedDoc; const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView);// if we have an annotation container and the target was hidden, then try again because we just un-hid the document above const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; - if (!docView && annoContainerView) { - annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below + if (annoContainerView) { + if (annoContainerView.props.Document.layoutKey === "layout_icon") { + annoContainerView.iconify(() => annoContainerView.focus(targetDoc, { + originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => { + focusAndFinish(true); + res(ViewAdjustment.doNothing); + }) + })); + return; + } else if (!docView && targetDoc.type !== DocumentType.MARKER) { + annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below + } } if (focusView) { !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox - focusView.focus(targetDoc, { + const doFocus = (forceDidFocus: boolean) => focusView.focus(originalTarget ?? targetDoc, { originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - focusAndFinish(didFocus); + focusAndFinish(forceDidFocus || didFocus); res(ViewAdjustment.doNothing); }) }); + if (focusView.props.Document.layoutKey === "layout_icon") { + focusView.iconify(() => doFocus(true)); + } else { + doFocus(false); + } } else { if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); } else { // otherwise try to get a view of the context of the target if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. + wasHidden = wasHidden || targetDocContextView.rootDoc.hidden; + targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden targetDocContext._viewTransition = "transform 500ms"; targetDocContextView.props.focus(targetDocContextView.rootDoc, { willZoom, afterFocus: async () => { targetDocContext._viewTransition = undefined; + if (targetDocContext.layoutKey === "layout_icon") { + targetDocContextView.iconify(() => this.jumpToDocument( + resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, + finished, originalTarget, noSelect, presZoom)); + } return ViewAdjustment.doNothing; } }); @@ -220,29 +249,38 @@ export class DocumentManager { finished?.(); } else { // no timecode means we need to find the context view and focus on our target const findView = (delay: number) => { - const retryDocView = getFirstDocView(targetDoc); // test again for the target view snce we presumably created the context above by focusing on it - if (retryDocView) { // we found the target in the context + const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it + if (retryDocView) { // we found the target in the context. Doc.linkFollowHighlight(retryDocView.rootDoc); - retryDocView.props.focus(targetDoc, { + retryDocView.focus(targetDoc, { willZoom, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { !noSelect && focusAndFinish(didFocus); res(ViewAdjustment.doNothing); }) }); // focus on the target in the context - } else if (delay > 1500) { + } else if (delay > 1000) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc); if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } } else { - setTimeout(() => findView(delay + 250), 250); + setTimeout(() => findView(delay + 200), 200); } }; - findView(0); + setTimeout(() => findView(0), 0); + } + } else { + if (docContext.length && docContext[0]?.layoutKey === "layout_icon") { + const docContextView = this.getFirstDocumentView(docContext[0]); + if (docContextView) { + return docContextView.iconify(() => this.jumpToDocument( + targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, + finished, originalTarget, noSelect, presZoom)); + } } - } else { // there's no context view so we need to create one first and try again when that finishes + // there's no context view so we need to create one first and try again when that finishes const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget); createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target finishFunc); @@ -250,18 +288,21 @@ export class DocumentManager { } } } - } -ScriptingGlobals.add(function DocFocusOrOpen(doc: any) { - const dv = DocumentManager.Instance.getDocumentView(doc); - if (dv && dv.props.Document === doc) { - dv.props.focus(doc, { willZoom: true }); +export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) { + const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc); + const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView); + if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) { + dv.props.focus(dv.props.Document, { willZoom: true }); Doc.linkFollowHighlight(dv?.props.Document, false); } else { - const context = doc.context !== Doc.UserDoc().myFilesystem && Cast(doc.context, Doc, null); + const context = doc.context !== CurrentUserUtils.MyFilesystem && Cast(doc.context, Doc, null); const showDoc = context || doc; - CollectionDockingView.AddSplit(showDoc === Doc.GetProto(showDoc) ? Doc.MakeAlias(showDoc) : showDoc, "right") && context && + const bestAlias = showDoc === Doc.GetProto(showDoc) ? DocListCast(showDoc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : showDoc; + + CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), "right") && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc)); } -});
\ No newline at end of file +} +ScriptingGlobals.add(DocFocusOrOpen);
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c9c499fff..09b463c2f 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -6,8 +6,8 @@ import { PrefetchProxy } from "../../fields/Proxy"; import { listSpec } from "../../fields/Schema"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ScriptField } from "../../fields/ScriptField"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; +import { emptyFunction, Utils } from "../../Utils"; import { Docs, DocUtils } from "../documents/Documents"; import * as globalCssVariables from "../views/global/globalCssVariables.scss"; import { DocumentView } from "../views/nodes/DocumentView"; @@ -56,12 +56,7 @@ export function SetupDrag( if (e.shiftKey) { e.persist(); const dragDoc = await docFunc(); - dragDoc && DragManager.StartWindowDrag?.({ - pageX: e.pageX, - pageY: e.pageY, - preventDefault: emptyFunction, - button: 0 - }, [dragDoc]); + dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); @@ -74,8 +69,11 @@ export function SetupDrag( export namespace DragManager { let dragDiv: HTMLDivElement; let dragLabel: HTMLDivElement; - export let StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined; + export let StartWindowDrag: Opt<((e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void)>; + export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; + export function GetRaiseWhenDragged() { return BoolCast(Doc.UserDoc()._raiseWhenDragged); } + export function SetRaiseWhenDragged(val:boolean) { Doc.UserDoc()._raiseWhenDragged = val } export function Root() { const root = document.getElementById("root"); if (!root) { @@ -225,10 +223,13 @@ export namespace DragManager { if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = - await Promise.all(dragData.draggedDocuments.map(async d => !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : - docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : - docDragData.dropAction === "proto" ? Doc.GetProto(d) : - docDragData.dropAction === "copy" ? (await Doc.MakeClone(d)).clone : d)); + await Promise.all(dragData.draggedDocuments.map(async d => + !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? + addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : + docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : + docDragData.dropAction === "proto" ? Doc.GetProto(d) : + docDragData.dropAction === "copy" ? + (await Doc.MakeClone(d)).clone : d)); !["same", "proto"].includes(docDragData.dropAction as any) && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []); const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); @@ -281,7 +282,7 @@ export namespace DragManager { SnappingManager.setSnapLines(horizLines, vertLines); } export function snapDragAspect(dragPt: number[], snapAspect: number) { - let closest = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); + let closest = Utils.SNAP_THRESHOLD; let near = dragPt; const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => { if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0 @@ -316,7 +317,7 @@ export namespace DragManager { } // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { - const snapThreshold = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); + const snapThreshold = Utils.SNAP_THRESHOLD; const snapVal = (pts: number[], drag: number, snapLines: number[]) => { if (snapLines.length) { const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt @@ -383,8 +384,8 @@ export namespace DragManager { } } const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / ele.offsetWidth; - const scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX; + const scaleX = rect.width / (ele.offsetWidth || rect.width); + const scaleY = ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX; elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); @@ -408,7 +409,7 @@ export namespace DragManager { Array.from(pdfBox).filter(pb => pb.width && pb.height).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => - ele.hasAttribute("style") && ((ele as any).style.pointerEvents = "none")); + (ele as any).style && ((ele as any).style.pointerEvents = "none")); dragDiv.appendChild(dragElement); if (dragElement !== ele) { @@ -453,24 +454,29 @@ export namespace DragManager { SnappingManager.clearSnapLines(); batch.end(); }); - const moveHandler = async (e: PointerEvent) => { + var startWindowDragTimer: any; + const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; } - if (e?.shiftKey && dragData.draggedDocuments.length === 1) { - dragData.dropAction = dragData.userDropAction || "same"; - if (dragData.dropAction === "move") { - dragData.removeDocument?.(dragData.draggedDocuments[0]); + if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { + if (!startWindowDragTimer) { + startWindowDragTimer = setTimeout(async () => { + startWindowDragTimer = undefined; + dragData.dropAction = dragData.userDropAction || "same"; + AbortDrag(); + await finishDrag?.(new DragCompleteEvent(true, dragData)); + DragManager.StartWindowDrag?.(e, dragData.droppedDocuments, (aborted) => { + if (!aborted && (dragData.dropAction === "move" || dragData.dropAction === "same")) { + dragData.removeDocument?.(dragData.draggedDocuments[0]); + } + }); + }, 500); } - AbortDrag(); - await finishDrag?.(new DragCompleteEvent(true, dragData)); - DragManager.StartWindowDrag?.({ - pageX: e.pageX, - pageY: e.pageY, - preventDefault: emptyFunction, - button: 0 - }, dragData.droppedDocuments); + } else { + clearTimeout(startWindowDragTimer); + startWindowDragTimer = undefined; } const target = document.elementFromPoint(e.x, e.y); @@ -536,6 +542,8 @@ export namespace DragManager { ); }; const upHandler = (e: PointerEvent) => { + clearTimeout(startWindowDragTimer); + startWindowDragTimer = undefined; dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, cleanupDrag); }; document.addEventListener("pointermove", moveHandler, true); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 082b6d8bd..256ab5c44 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -10,12 +10,17 @@ import { ImageField } from "../../fields/URLField"; import { ScriptingGlobals } from "./ScriptingGlobals"; import { listSpec } from "../../fields/Schema"; +export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") { + if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated + doc.isTemplateDoc = makeTemplate(doc, first, rename); + return doc; +} // // converts 'doc' into a template that can be used to render other documents. // the title of doc is used to determine which field is being templated, so // passing a value for 'rename' allows the doc to be given a meangingful name // after it has been converted to -export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean { +function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.layout instanceof Doc) { // its already a template return true; diff --git a/src/client/util/History.ts b/src/client/util/History.ts index e6f75a7db..7dcff9c56 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -58,8 +58,10 @@ export namespace HistoryUtil { export function getState(): ParsedUrl { const state = copyState(history.state); - state.initializers = state.initializers || {}; - return state; + if (state) { + state.initializers = state.initializers || {}; + } + return state ?? {initializers:{}}; } // export function addHandler(handler: (state: ParsedUrl | null) => void) { @@ -195,7 +197,7 @@ export namespace HistoryUtil { await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); } if (field instanceof Doc) { - CurrentUserUtils.openDashboard(Doc.UserDoc(), field, true); + CurrentUserUtils.openDashboard(field, true); } } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 39e9251a5..37571ae01 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -161,7 +161,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); - DocumentManager.Instance.jumpToDocument(importContainer, true); + DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []); } runInAction(() => { diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 3e051dec8..289c5bc51 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -286,6 +286,8 @@ export namespace InteractionUtils { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); + case TOUCHTYPE: + return e.pointerType === TOUCHTYPE; default: return e.pointerType === type; } } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index df2c02a8d..d51cd350d 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,6 +1,6 @@ import { action, observable, observe } from "mobx"; import { computedFn } from "mobx-utils"; -import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc"; +import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { ProxyField } from "../../fields/Proxy"; import { BoolCast, Cast, StrCast } from "../../fields/Types"; @@ -29,76 +29,79 @@ export class LinkManager { public static currentLink: Opt<Doc>; public static get Instance() { return LinkManager._instance; } public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb); + public static AutoKeywords = "keywords:Usages"; static links: Doc[] = []; constructor() { LinkManager._instance = this; this.createLinkrelationshipLists(); - setTimeout(() => { - LinkManager.userLinkDBs = []; - const addLinkToDoc = (link: Doc) => { - const a1Prom = link?.anchor1; - const a2Prom = link?.anchor2; - Promise.all([a1Prom, a2Prom]).then((all) => { - const a1 = all[0]; - const a2 = all[1]; - const a1ProtoProm = (link?.anchor1 as Doc)?.proto; - const a2ProtoProm = (link?.anchor2 as Doc)?.proto; - Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => { - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].add(link); - Doc.GetProto(a2)[DirectLinksSym].add(link); - Doc.GetProto(link)[DirectLinksSym].add(link); - } - })); - }); - }; - const remLinkFromDoc = (link: Doc) => { - const a1 = link?.anchor1; - const a2 = link?.anchor2; - Promise.all([a1, a2]).then(action(() => { + LinkManager.userLinkDBs = []; + const addLinkToDoc = (link: Doc) => { + const a1Prom = link?.anchor1; + const a2Prom = link?.anchor2; + Promise.all([a1Prom, a2Prom]).then((all) => { + const a1 = all[0]; + const a2 = all[1]; + const a1ProtoProm = (link?.anchor1 as Doc)?.proto; + const a2ProtoProm = (link?.anchor2 as Doc)?.proto; + Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => { if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].delete(link); - Doc.GetProto(a2)[DirectLinksSym].delete(link); - Doc.GetProto(link)[DirectLinksSym].delete(link); + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + Doc.GetProto(link)[DirectLinksSym].add(link); } })); - }; - const watchUserLinkDB = (userLinkDBDoc: Doc) => { - LinkManager.links.push(...DocListCast(userLinkDBDoc.data)); - const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields - observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) - switch (change.type as any) { - case "splice": - (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); - (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); - break; - case "update": //let oldValue = change.oldValue; - } - }, true); - observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) - change => { - switch (change.type as any) { - case "update": - Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => { - const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length); - const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length); - - const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); - const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); - added?.forEach((link: any) => addLinkToDoc(toRealField(link))); - removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); - }); - } - }, true); - }; - observe(LinkManager.userLinkDBs, change => { + }); + }; + const remLinkFromDoc = (link: Doc) => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + Promise.all([a1, a2]).then(action(() => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + Doc.GetProto(link)[DirectLinksSym].delete(link); + } + })); + }; + const watchUserLinkDB = (userLinkDBDoc: Doc) => { + LinkManager.links.push(...DocListCast(userLinkDBDoc.data)); + const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) switch (change.type as any) { - case "splice": (change as any).added.forEach(watchUserLinkDB); break; + case "splice": + (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); + (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); + break; case "update": //let oldValue = change.oldValue; } }, true); - LinkManager.addLinkDB(Doc.LinkDBDoc()); - }); + observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) + change => { + switch (change.type as any) { + case "update": + Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => { + const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length); + const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length); + + const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); + const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); + added?.forEach((link: any) => addLinkToDoc(toRealField(link))); + removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); + }); + } + }, true); + }; + observe(LinkManager.userLinkDBs, change => { + switch (change.type as any) { + case "splice": (change as any).added.forEach(watchUserLinkDB); break; + case "update": //let oldValue = change.oldValue; + } + }, true); + LinkManager.addLinkDB(Doc.LinkDBDoc()); + DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist => dblist?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager + const a1 = await Cast(link?.anchor1, Doc, null); + const a2 = await Cast(link?.anchor2, Doc, null); + })); } @@ -149,9 +152,11 @@ export class LinkManager { public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { const anchorGroups = new Map<string, Array<Doc>>(); this.relatedLinker(anchor).forEach(link => { - if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") { - const group = anchorGroups.get(StrCast(link.linkRelationship)); - anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]); + if (link.linkRelationship && link.linkRelationship !== "-ungrouped-") { + const relation = StrCast(link.linkRelationship); + const anchorRelation = relation.indexOf(":") !== -1 ? relation.split(":")[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation; + const group = anchorGroups.get(anchorRelation); + anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { // if link is in no groups then put it in default group const group = anchorGroups.get("*"); @@ -221,7 +226,9 @@ export class LinkManager { const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs); - const followLinks = linkDocList.length ? (sourceDoc.isPushpin ? linkDocList : [linkDocList[0]]) : []; + const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1); + var count = 0; + const allFinished = () => ++count === followLinks.length && finished?.(); followLinks.forEach(async linkDoc => { if (linkDoc) { const target = (sourceDoc === linkDoc.anchor1 ? linkDoc.anchor2 : sourceDoc === linkDoc.anchor2 ? linkDoc.anchor1 : @@ -232,20 +239,22 @@ export class LinkManager { const tour = DocListCast(target[fieldKey]).reverse(); LightboxView.SetLightboxDoc(currentContext, undefined, tour); setTimeout(LightboxView.Next); - finished?.(); + allFinished(); } else { const containerAnnoDoc = Cast(target.annotationOn, Doc, null); const containerDoc = containerAnnoDoc || target; - const containerDocContext = Cast(containerDoc?.context, Doc, null); - const targetContext = LightboxView.LightboxDoc ? containerAnnoDoc || containerDocContext : containerDocContext; - const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); + var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : [] as Doc[]; + while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { + containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + } + const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished); } } else { - finished?.(); + allFinished(); } } else { - finished?.(); + allFinished(); } }); } diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts new file mode 100644 index 000000000..021feee9a --- /dev/null +++ b/src/client/util/RecordingApi.ts @@ -0,0 +1,269 @@ +import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; +import { IReactionDisposer, observable, reaction } from "mobx"; +import { NumCast } from "../../fields/Types"; +import { Doc } from "../../fields/Doc"; +import { VideoBox } from "../views/nodes/VideoBox"; +import { scaleDiverging } from "d3-scale"; +import { Transform } from "./Transform"; + +type Movement = { + time: number, + panX: number, + panY: number, + scale: number, +} + +type Presentation = { + movements: Array<Movement> | null + meta: Object, +} + +export class RecordingApi { + + private static NULL_PRESENTATION: Presentation = { + movements: null, + meta: {}, + } + + // instance variables + private currentPresentation: Presentation; + private isRecording: boolean; + private absoluteStart: number; + + + // create static instance and getter for global use + @observable static _instance: RecordingApi; + public static get Instance(): RecordingApi { return RecordingApi._instance } + public constructor() { + // init the global instance + RecordingApi._instance = this; + + // init the instance variables + this.currentPresentation = RecordingApi.NULL_PRESENTATION + this.isRecording = false; + this.absoluteStart = -1; + + // used for tracking movements in the view frame + this.disposeFunc = null; + this.recordingFFView = null; + + // for now, set playFFView + this.playFFView = null; + this.timers = null; + } + + // little helper :) + private get isInitPresenation(): boolean { + return this.currentPresentation.movements === null + } + + public start = (meta?: Object): Error | undefined => { + // check if already init a presentation + if (!this.isInitPresenation) { + console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') + return new Error('[recordingApi.ts] start()') + } + + // update the presentation mode + Doc.UserDoc().presentationMode = 'recording' + + // (1a) get start date for presenation + const startDate = new Date() + // (1b) set start timestamp to absolute timestamp + this.absoluteStart = startDate.getTime() + + // (2) assign meta content if it exists + this.currentPresentation.meta = meta || {} + // (3) assign start date to currentPresenation + this.currentPresentation.movements = [] + // (4) set isRecording true to allow trackMovements + this.isRecording = true + } + + public clear = (): Error | Presentation => { + // TODO: maybe archive the data? + if (this.isRecording) { + console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first') + return new Error('[recordingApi.ts] clear()') + } + + // update the presentation mode + Doc.UserDoc().presentationMode = 'none' + // set the previus recording view to the play view + this.playFFView = this.recordingFFView + + const presCopy = { ...this.currentPresentation } + + // clear presenation data + this.currentPresentation = RecordingApi.NULL_PRESENTATION + // clear isRecording + this.isRecording = false + // clear absoluteStart + this.absoluteStart = -1 + // clear the disposeFunc + this.removeRecordingFFView() + + return presCopy; + } + + public pause = (): Error | undefined => { + if (this.isInitPresenation) { + console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first') + return new Error('[recordingApi.ts] pause(): no presentation') + } + // don't allow track movments + this.isRecording = false + + // set adjust absoluteStart to add the time difference + const timestamp = new Date().getTime() + this.absoluteStart = timestamp - this.absoluteStart + } + + public resume = () => { + this.isRecording = true + // set absoluteStart to the difference in time + this.absoluteStart = new Date().getTime() - this.absoluteStart + } + + private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => { + // ensure we are recording + if (!this.isRecording) { + return new Error('[recordingApi.ts] trackMovements()') + } + // check to see if the presetation is init + if (this.isInitPresenation) { + return new Error('[recordingApi.ts] trackMovements(): no presentation') + } + + // get the time + const time = new Date().getTime() - this.absoluteStart + // make new movement object + const movement: Movement = { time, panX, panY, scale } + + // add that movement to the current presentation data's movement array + this.currentPresentation.movements && this.currentPresentation.movements.push(movement) + } + + // instance variable for the FFView + private disposeFunc: IReactionDisposer | null; + private recordingFFView: CollectionFreeFormView | null; + + // set the FFView that will be used in a reaction to track the movements + public setRecordingFFView = (view: CollectionFreeFormView): void => { + // set the view to the current view + if (view === this.recordingFFView || view == null) return; + + // this.recordingFFView = view; + // set the reaction to track the movements + this.disposeFunc = reaction( + () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }), + (res) => (res.x !== -1 && res.y !== -1 && this.isRecording) && this.trackMovements(res.x, res.y, res.scale) + ) + + // for now, set the most recent recordingFFView to the playFFView + this.recordingFFView = view; + } + + // call on dispose function to stop tracking movements + public removeRecordingFFView = (): void => { + this.disposeFunc?.(); + this.disposeFunc = null; + } + + // TODO: extract this into different class with pause and resume recording + // TODO: store the FFview with the movements + private playFFView: CollectionFreeFormView | null; + private timers: NodeJS.Timeout[] | null; + + public setPlayFFView = (view: CollectionFreeFormView): void => { + this.playFFView = view + } + + // pausing movements will dispose all timers that are planned to replay the movements + // play movemvents will recreate them when the user resumes the presentation + public pauseMovements = (): undefined | Error => { + if (this.playFFView === null) { + return new Error('[recordingApi.ts] pauseMovements() failed: no view') + } + + if (!this._isPlaying) { + //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') + return + } + this._isPlaying = false + // TODO: set userdoc presentMode to browsing + this.timers?.map(timer => clearTimeout(timer)) + + // this.videoBox = null; + } + + private videoBox: VideoBox | null = null; + + // by calling pause on the VideoBox, the pauseMovements will be called + public pauseVideoAndMovements = (): boolean => { + this.videoBox?.Pause() + + this.pauseMovements() + return this.videoBox == null + } + + public _isPlaying = false; + + public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { + if (presentation.movements === null || this.playFFView === null) { + return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') + } + if(this._isPlaying) return; + + this._isPlaying = true; + Doc.UserDoc().presentationMode = 'watching'; + + // TODO: consider this bug at the end of the clip on seek + this.videoBox = videoBox || null; + + // only get the movements that are remaining in the video time left + const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) + + // helper to replay a movement + const document = this.playFFView + let preScale = -1; + const zoomAndPan = (movement: Movement) => { + const { panX, panY, scale } = movement; + (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); + document.Document._panX = panX; + document.Document._panY = panY; + + preScale = scale; + } + + // set the first frame to be at the start of the pres + zoomAndPan(filteredMovements[0]); + + // make timers that will execute each movement at the correct replay time + this.timers = filteredMovements.map(movement => { + const timeDiff = movement.time - timeViewed*1000 + return setTimeout(() => { + // replay the movement + zoomAndPan(movement) + // if last movement, presentation is done -> set the instance var + if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; + }, timeDiff) + }) + } + + // Unfinished code for tracing multiple free form views + // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map() + + // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { + // pres.set(ffView, + // reaction(() => ({ x: ffView.panX, y: ffView.panY }), + // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) + // ) + // } + + // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { + // const disposer = pres.get(ffView); + // disposer?.(); + // pres.delete(ffView) + // } +} diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 3b0a47b54..3791bec73 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -223,7 +223,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } const paramString = paramList.join(", "); - const body = addReturn ? `return ${script};` : `return ${script};`; + const body = addReturn && !script.startsWith("{ return") ? `return ${script};` : script; const reqTypes = requiredType ? `: ${requiredType}` : ''; const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile("file.ts", funcScript); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 6a26dfdc7..080237649 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -10,7 +10,9 @@ import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager import { DocServer } from "../DocServer"; import { Networking } from "../Network"; import { MainViewModal } from "../views/MainViewModal"; +import { FontIconBox } from "../views/nodes/button/FontIconBox"; import { CurrentUserUtils } from "./CurrentUserUtils"; +import { DragManager } from "./DragManager"; import { GroupManager } from "./GroupManager"; import "./SettingsManager.scss"; import { undoBatch } from "./UndoManager"; @@ -38,7 +40,7 @@ export class SettingsManager extends React.Component<{}> { @observable activeTab = "Accounts"; @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } - @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard.colorScheme; } + @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme; } constructor(props: {}) { super(props); @@ -59,18 +61,17 @@ export class SettingsManager extends React.Component<{}> { } } - @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.UserDoc().noviceMode = (e.currentTarget as any)?.value === "Novice"); + @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.noviceMode = (e.currentTarget as any)?.value === "Novice"); @undoBatch changeShowTitle = action((e: React.ChangeEvent) => Doc.UserDoc().showTitle = (e.currentTarget as any).value ? "title" : undefined); @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value); @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value); @undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); @undoBatch switchUserColor = action((color: ColorState) => { Doc.SharingDoc().userColor = undefined; Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex); }); - @undoBatch - playgroundModeToggle = action(() => { + @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; if (this.playgroundMode) { DocServer.Control.makeReadOnly(); - addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" }); + addStyleSheetRule(SettingsManager._settingsStyle, "topbar-inner-container", { background: "red !important" }); } else DocServer.Control.makeEditable(); }); @@ -78,19 +79,21 @@ export class SettingsManager extends React.Component<{}> { @undoBatch @action changeColorScheme = action((e: React.ChangeEvent) => { + const activeDashboard= CurrentUserUtils.ActiveDashboard; + if (!activeDashboard) return; const scheme: ColorScheme = (e.currentTarget as any).value; switch (scheme) { case ColorScheme.Light: - CurrentUserUtils.ActiveDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) + activeDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" }); break; case ColorScheme.Dark: - CurrentUserUtils.ActiveDashboard.colorScheme = ColorScheme.Dark; + activeDashboard.colorScheme = ColorScheme.Dark; addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" }); break; case ColorScheme.System: default: window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { - CurrentUserUtils.ActiveDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) + activeDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) }); break; } @@ -153,19 +156,14 @@ export class SettingsManager extends React.Component<{}> { <div className="preferences-check">Show full toolbar</div> </div> <div> - <input type="checkbox" onChange={e => Doc.UserDoc()._raiseWhenDragged = !Doc.UserDoc()._raiseWhenDragged} - checked={BoolCast(Doc.UserDoc()._raiseWhenDragged)} /> + <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} + checked={DragManager.GetRaiseWhenDragged()} /> <div className="preferences-check">Raise on drag</div> </div> <div> - <input type="checkbox" onChange={e => Doc.UserDoc()._showLabel = !Doc.UserDoc()._showLabel} - checked={BoolCast(Doc.UserDoc()._showLabel)} /> - <div className="preferences-check">Show tool button labels</div> - </div> - <div> - <input type="checkbox" onChange={e => Doc.UserDoc()._showMenuLabel = !Doc.UserDoc()._showMenuLabel} - checked={BoolCast(Doc.UserDoc()._showMenuLabel)} /> - <div className="preferences-check">Show menu button labels</div> + <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} + checked={FontIconBox.GetShowLabels()} /> + <div className="preferences-check">Show button labels</div> </div> </div>; } @@ -254,7 +252,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column"> <div className="tab-column-title">Modes</div> <div className="tab-column-content"> - <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.UserDoc().noviceMode ? "Novice" : "Developer"}> + <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.noviceMode ? "Novice" : "Developer"}> <option key={"Novice"} value={"Novice"}> Novice </option> <option key={"Developer"} value={"Developer"}> Developer</option> </select> @@ -269,8 +267,8 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button> <div className="default-acl"> - <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} - onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} /> + <input className="acl-check" type="checkbox" checked={BoolCast(Doc.defaultAclPrivate)} + onChange={action(() => Doc.defaultAclPrivate = !Doc.defaultAclPrivate)} /> <div className="acl-text">Default access private</div> </div> </div> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 6d7f7e8df..d77633b8d 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -369,12 +369,12 @@ export class SharingManager extends React.Component<{}> { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift("-multiple-"); if (override) dropdownValues.unshift("None"); - return dropdownValues.filter(permission => !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission => - ( - <option key={permission} value={permission}> - {permission} - </option> - ) + return dropdownValues.filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission => + ( + <option key={permission} value={permission}> + {permission} + </option> + ) ); } @@ -388,7 +388,7 @@ export class SharingManager extends React.Component<{}> { onClick={() => { let context: Opt<CollectionView>; if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) { - DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, context.props.Document); + DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]); } }} onPointerEnter={action(() => { @@ -548,10 +548,10 @@ export class SharingManager extends React.Component<{}> { {this.sharingOptions(uniform)} </select> ) : ( - <div className={"permissions-dropdown"}> - {permissions} - </div> - )} + <div className={"permissions-dropdown"}> + {permissions} + </div> + )} </div> </div> ); @@ -572,7 +572,7 @@ export class SharingManager extends React.Component<{}> { <div className="edit-actions"> <div className={"permissions-dropdown"}> Owner - </div> + </div> </div> </div> ) : null, @@ -622,10 +622,10 @@ export class SharingManager extends React.Component<{}> { {this.sharingOptions(uniform, group.title === "Override")} </select> ) : ( - <div className={"permissions-dropdown"}> - {permissions} - </div> - )} + <div className={"permissions-dropdown"}> + {permissions} + </div> + )} </div> </div> ); @@ -658,6 +658,7 @@ export class SharingManager extends React.Component<{}> { isSearchable closeMenuOnSelect={false} options={options} + onKeyDown={e => e.stopPropagation()} onChange={this.handleUsersChange} value={this.selectedUsers} styles={{ @@ -679,7 +680,7 @@ export class SharingManager extends React.Component<{}> { </div> <div className="acl-container"> - {Doc.UserDoc().noviceMode ? (null) : + {Doc.noviceMode ? (null) : <div className="layoutDoc-acls"> <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> </div>} diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 069f81d38..057843c68 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,5 +1,6 @@ import { observable, action, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; +import { Doc } from "../../fields/Doc"; export namespace SnappingManager { @@ -30,6 +31,9 @@ export namespace SnappingManager { export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } export function GetIsDragging() { return manager.IsDragging; } + export function SetShowSnapLines(show: boolean) { runInAction(() => Doc.UserDoc().showSnapLines = show); } + export function GetShowSnapLines() { return Doc.UserDoc().showSnapLines; } + /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts // need to investigate further what caused the mobx update problems and move to a better location. const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true); |
