import { computed, observable, reaction } from 'mobx'; import * as rp from 'request-promise'; import { DataSym, Doc, DocListCast, DocListCastAsync, Opt, } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { RichTextField } from '../../fields/RichTextField'; import { listSpec } from '../../fields/Schema'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; import { Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast, } from '../../fields/Types'; import { ImageField, nullAudio } from '../../fields/URLField'; import { SharingPermissions } from '../../fields/util'; import { OmitKeys, Utils } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils, FInfo } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; 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 { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../views/nodes/DocumentView'; import { OverlayView } from '../views/OverlayView'; import { DocumentManager } from './DocumentManager'; import { DragManager } from './DragManager'; import { makeTemplate, 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'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; interface Button { // DocumentOptions fields a button can set title?: string; toolTip?: string; icon?: string; btnType?: ButtonType; numBtnType?: NumButtonType; numBtnMin?: number; numBtnMax?: number; switchToggle?: boolean; width?: number; btnList?: List; 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 }; export class CurrentUserUtils { private static curr_id: string; //TODO tfs: these should be temporary... private static mainDocId: string | undefined; 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; } @computed public static get UserDocument() { return Doc.UserDoc(); } @observable public static GuestTarget: Doc | undefined; @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; 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 ); } // 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, btnOpts: DocumentOptions, templateOptions: DocumentOptions ) => { if (templateBtn) { this.AssignOpts(templateBtn, btnOpts); this.AssignDocField( templateBtn, 'dragFactory', (opts) => template(opts), templateOptions ); } return templateBtn; }; const makeTemp = (doc: Doc) => { doc.isTemplateDoc = makeTemplate(doc); return doc; }; return this.AssignScripts( assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton({ ...btnOpts, dragFactory: makeTemp(template(templateOpts)), }), reqdScripts ); } ); 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 ); } /// 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; const makeTemp = (doc: Doc, noteType?: string) => { doc.isTemplateDoc = makeTemplate(doc, true, noteType ?? 'Note'); return doc; }; return ( this.AssignOpts(noteType, reqdOpts) ?? makeTemp(Docs.Create.TextDocument('', reqdOpts), opts.noteType) ); }); const reqdOpts: DocumentOptions = { title: 'Note Layouts', _height: 75, system: true, }; return ( this.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)) ); } /// 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, 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, }); // prettier-ignore 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" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation static creatorBtnDescriptors(doc: 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 }; }[] { const standardOps = (key: string) => ({ title: 'Untitled ' + key, _fitWidth: true, system: true, 'dragFactory-count': 0, cloneFieldFilter: new List(['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 headerBtnHgt = 10; const headerTemplate = (opts: DocumentOptions) => { const header = Docs.Create.RTFDocument( new RichTextField(JSON.stringify(json), ''), { ...opts, title: 'text', layout: "" + ` ` + " " + ` Metadata` + '', }, 'header' ); // "
" + // " " + // " " + // "
"; Doc.GetProto(header).isTemplateDoc = makeTemplate( Doc.GetProto(header), true, 'headerView' ); Doc.GetProto(header).title = 'Untitled Header'; return header; }; // prettier-ignore 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(["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 ) ); // prettier-ignore 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, })) } /// 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([ '_stayInCollection', ]), }; return this.AssignScripts( this.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs ); } ); 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 ); } /// 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())"; // prettier-ignore return [ { 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)'}})); } /// 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([ 'dropAction', '_stayInCollection', ]), }; return this.AssignScripts( this.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs ); }); 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 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 static setupMobileButtons(doc?: Doc, buttons?: string[]) { return []; // prettier-ignore 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." }, { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." }, { title: "RECORD", icon: "microphone", click: 'openMobileAudio()', backgroundColor: "lightgrey", info: "Use your phone to record, dictate and then upload audio onto Dash Web." }, { title: "PRESENTATION", icon: "desktop", click: 'switchToMobilePresentation()', backgroundColor: "lightgrey", info: "Use your phone as a remote for you presentation." }, { title: "SETTINGS", icon: "cog", click: 'openMobileSettings()', backgroundColor: "lightgrey", info: "Change your password, log out, or manage your account security." } ]; // returns a list of mobile buttons return docProtoData .filter((d) => !buttons || !buttons.includes(d.title)) .map((data) => this.mobileButton( { title: data.title, _lockedPosition: true, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, backgroundColor: data.backgroundColor, system: true, }, [ this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: 'rgba(0,0,0,0)', system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [ this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info), ]), ] ) ); } // sets up the main document for the mobile button static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { ...opts, _removeDropProperties: new List(['dropAction']), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, borderRounding: '5px', boxShadow: '0 0', system: true, }) as any as Doc; // sets up the text container for the information contained within the mobile button static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { ...opts, _removeDropProperties: new List(['dropAction']), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, backgroundColor: 'rgba(0,0,0,0)', borderRounding: '0', boxShadow: '0 0', ignoreClick: true, system: true, }) as any as Doc; // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, title: buttonTitle, _fontSize: '37px', _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: 'rgba(0,0,0,0)', system: true, }) as any as Doc; // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, title: 'info', _fontSize: '25px', _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: 'rgba(0,0,0,0)', _dimMagnitude: 2, system: true, }) as any as Doc; static setupThumbButtons(doc: Doc) { // prettier-ignore const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, clipboard?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue" }, { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow" }, { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }), backgroundColor: "orange" }, { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange" }, { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green" }, ]; return docProtoData.map((data) => Docs.Create.FontIconDocument({ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon, _dropAction: data.pointerDown ? 'copy' : undefined, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, clipboard: data.clipboard, onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, backgroundColor: data.backgroundColor, _removeDropProperties: new List(['dropAction']), dragFactory: data.dragFactory, system: true, }) ); } static setupThumbDoc(userDoc: Doc) { if (!userDoc.thumbDoc) { const thumbDoc = Docs.Create.LinearDocument( CurrentUserUtils.setupThumbButtons(userDoc), { _width: 100, _height: 50, ignoreClick: true, _lockedPosition: true, title: 'buttons', _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: 'white', system: true, } ); thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, _autoHeight: true, linearViewIsExpanded: true, flexDirection: 'column', system: true, }); userDoc.thumbDoc = thumbDoc; } return Cast(userDoc.thumbDoc, Doc); } static setupMobileInkingDoc(userDoc: Doc) { return Docs.Create.FreeformDocument([], { title: 'Mobile Inking', backgroundColor: 'white', system: true, }); } static setupMobileUploadDoc(userDoc: Doc) { // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" }) const webDoc = Docs.Create.WebDocument( 'https://www.britannica.com/biography/Miles-Davis', { title: 'Upload Images From the Web', _lockedPosition: true, system: true, } ); const uploadDoc = Docs.Create.StackingDocument([], { title: 'Mobile Upload Collection', backgroundColor: 'white', _lockedPosition: true, system: true, _chromeHidden: true, }); return Docs.Create.StackingDocument([webDoc, uploadDoc], { _width: screen.width, _lockedPosition: true, title: 'Upload', _autoHeight: true, _yMargin: 80, backgroundColor: 'lightgray', system: true, _chromeHidden: true, }); } /// 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, } ); } /// 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] ); } /// 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(['Create New Dashboard']), contextMenuIcons: new List(['plus']), childContextMenuLabels: new List([ '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([ '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( contextMenuScripts.map( (script) => ScriptField.MakeFunction(script)! ) ); } if ( Cast( myDashboards.childContextMenuScripts, listSpec(ScriptField), null )?.length !== childContextMenuScripts.length ) { myDashboards.childContextMenuScripts = new List( childContextMenuScripts.map( (script) => ScriptField.MakeFunction(script)! ) ); } if ( Cast( myDashboards.childContextMenuFilters, listSpec(ScriptField), null )?.length !== childContextMenuFilters.length ) { myDashboards.childContextMenuFilters = new List( childContextMenuFilters.map((script) => !script ? script : ScriptField.MakeFunction(script)! ) ); } return myDashboards; } /// 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(['plus']), contextMenuLabels: new List(['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( contextMenuScripts.map( (script) => ScriptField.MakeFunction(script)! ) ); } return myTrails; } /// 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(['Create new folder']), childContextMenuIcons: new List(['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( childContextMenuScripts.map( (script) => ScriptField.MakeFunction(script)! ) ); } return myFilesystem; } /// 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(['Empty recently closed']), contextMenuIcons: new List(['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.MakeScript(clearAll('self'))!, ]); } return recentlyClosed; } /// 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(['Clear All']), childContextMenuScripts: new List([ ScriptField.MakeFunction(clearAll)!, ]), }; return Docs.Create.FilterDocument(reqdOpts); } /// 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[]) => 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', }); static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: 'alias', _hideContextMenu: true, _removeDropProperties: new List([ '_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 ); } // prettier-ignore static textTools():Button[] { return [ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'}, btnList: new List(["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_);}'}}, ]; } // prettier-ignore 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_);}'} }, ]; } 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_);}', }, }, ]; } // prettier-ignore 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_); }'} }, ]; } // prettier-ignore static contextMenuTools():Button[] { return [ { btnList: new 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]), 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 ]; } /// 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([ '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 ); } /// 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, }; return this.AssignScripts( this.AssignOpts(menuBtnDoc, reqdSubMenuOpts) ?? (ctxtMenuBtnsDoc[StrCast(params.title)] = this.linearButtonList( reqdSubMenuOpts, params.subMenu.map((sub) => this.setupContextMenuButton( sub, DocListCast(menuBtnDoc?.data).find( (doc) => doc.title === sub.title ) ) ) )), undefined, params.funcs ); } } ); return this.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } /// 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, } ); } 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([]); linkDocs['acl-Public'] = SharingPermissions.Augment; doc.myLinkDatabase = new PrefetchProxy(linkDocs); } } /// 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([dashboardFilter!]), childContextMenuScripts: new List([addToDashboards!]), childContextMenuLabels: new List(['Add to Dashboards']), childContextMenuIcons: new List(['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 ); } /// 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) { if (doc['clickFuncs-child'] === undefined) { // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target const openInTarget = Docs.Create.ScriptingDocument( ScriptField.MakeScript( 'docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ', { thisContainer: Doc.name } ), { title: 'Click to open in target', _width: 300, _height: 200, targetScriptKey: 'onChildClick', system: true, } ); const openDetail = Docs.Create.ScriptingDocument( ScriptField.MakeScript('openOnRight(self.doubleClickView)', {}), { title: 'Double click to open doubleClickView', _width: 300, _height: 200, targetScriptKey: 'onChildDoubleClick', system: true, } ); doc['clickFuncs-child'] = Docs.Create.TreeDocument( [openInTarget, openDetail], { title: 'on Child Click function templates', system: true } ); } // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved. PromiseValue(Cast(doc['clickFuncs-child'], Doc)).then( (func) => func && PromiseValue(func.data).then(DocListCast) ); if (doc.clickFuncs === undefined) { const onClick = Docs.Create.ScriptingDocument( undefined, { title: 'onClick', 'onClick-rawScript': "console.log('click')", isTemplateDoc: true, isTemplateForField: 'onClick', _width: 300, _height: 200, system: true, }, 'onClick' ); const onChildClick = Docs.Create.ScriptingDocument( undefined, { title: 'onChildClick', 'onChildClick-rawScript': "console.log('child click')", isTemplateDoc: true, isTemplateForField: 'onChildClick', _width: 300, _height: 200, system: true, }, 'onChildClick' ); const onDoubleClick = Docs.Create.ScriptingDocument( undefined, { title: 'onDoubleClick', 'onDoubleClick-rawScript': "console.log('double click')", isTemplateDoc: true, isTemplateForField: 'onDoubleClick', _width: 300, _height: 200, system: true, }, 'onDoubleClick' ); const onChildDoubleClick = Docs.Create.ScriptingDocument( undefined, { title: 'onChildDoubleClick', 'onChildDoubleClick-rawScript': "console.log('child double click')", isTemplateDoc: true, isTemplateForField: 'onChildDoubleClick', _width: 300, _height: 200, system: true, }, 'onChildDoubleClick' ); const onCheckedClick = Docs.Create.ScriptingDocument( undefined, { title: 'onCheckedClick', 'onCheckedClick-rawScript': 'console.log(heading + checked + containingTreeView)', 'onCheckedClick-params': new List([ 'heading', 'checked', 'containingTreeView', ]), isTemplateDoc: true, isTemplateForField: 'onCheckedClick', _width: 300, _height: 200, system: true, }, 'onCheckedClick' ); doc.clickFuncs = Docs.Create.TreeDocument( [onClick, onChildClick, onDoubleClick, onCheckedClick], { title: 'onClick funcs', system: true } ); } PromiseValue(Cast(doc.clickFuncs, Doc)).then( (func) => func && PromiseValue(func.data).then(DocListCast) ); return doc.clickFuncs as Doc; } /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field /// whether to revert to "default" values, or to leave them as the user/system last set them. 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( 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 } ); 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.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.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile 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 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; } 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( options.values as any ); break; case 'number': opts.fieldValues = new List( options.values as any ); break; case 'Doc': opts.fieldValues = new List(options.values as any); break; default: opts.fieldValues = new List( 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) => { if (response) { const result: { id: string; email: string; cacheDocumentIds: string; } = JSON.parse(response); Doc.CurrentUserEmail = result.email; resolvedPorts = JSON.parse( await (await fetch('/resolvedPorts')).text() ); DocServer.init( window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email ); result.cacheDocumentIds && (await DocServer.GetRefFields( result.cacheDocumentIds.split(';') )); return result; } else { throw new Error( "There should be a user! Why does Dash think there isn't one?" ); } }); } public static async loadUserDocument(id: string) { this.curr_id = id; await rp.get(Utils.prepend('/getUserDocumentIds')).then((ids) => { const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids); if (userDocumentId !== 'guest') { return DocServer.GetRefField(userDocumentId).then( async (field) => { Docs.newAccount = !(field instanceof Doc); await Docs.Prototypes.initialize(); const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : (field as Doc); 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?" ); } }); } public static _urlState: HistoryUtil.DocUrl; /// 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); // 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 && !Doc.UserDoc()) { DocServer.Control.makeReadOnly(); } else { fromHistory || HistoryUtil.pushState({ type: 'doc', docId: doc[Id], readonly: state.readonly, nro: state.nro, sharing: false, }); if (state.readonly === true || state.readonly === null) { DocServer.Control.makeReadOnly(); } else if (state.safe) { if (!state.nro) { DocServer.Control.makeReadOnly(); } CollectionView.SetSafeMode(true); } else if ( state.nro || state.nro === null || state.readonly === false ) { } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { DocServer.Control.makeEditable(); } } return true; }; public static importDocument = () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.accept = '.zip, application/pdf, video/*, image/*, audio/*'; input.onchange = async (_e) => { const upload = Utils.prepend('/uploadDoc'); const formData = new FormData(); 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 disposer = OverlayView.ShowSpinner(); 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 closeActiveDashboard = () => { CurrentUserUtils.ActiveDashboard = undefined; }; 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 freeformOptions: DocumentOptions = { x: 0, y: 400, _width: 1500, _height: 1000, _fitWidth: true, _backgroundGridShow: true, 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: title }, id, 'row' ); freeformDoc.context = dashboardDoc; // switching the tabs from the datadoc to the regular doc const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); dashboardDoc.data = new List(dashboardTabs); dashboardDoc['pane-count'] = 1; CurrentUserUtils.ActivePresentation = presentation; Doc.AddDocToList(dashboards, 'data', 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: 35, x: x, y: y, _fitWidth: true, _autoHeight: true, title, }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { tbox._width = NumCast(template._width); tbox.layoutKey = 'layout_' + StrCast(template.title); Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } return tbox; } 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 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() { 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) ); });