aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CaptureManager.tsx157
-rw-r--r--src/client/util/CurrentUserUtils.ts2085
-rw-r--r--src/client/util/DocumentManager.ts251
-rw-r--r--src/client/util/DragManager.ts376
-rw-r--r--src/client/util/DropConverter.ts7
-rw-r--r--src/client/util/GroupManager.tsx260
-rw-r--r--src/client/util/History.ts49
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx2
-rw-r--r--src/client/util/InteractionUtils.tsx2
-rw-r--r--src/client/util/LinkFollower.ts112
-rw-r--r--src/client/util/LinkManager.ts274
-rw-r--r--src/client/util/ReplayMovements.ts208
-rw-r--r--src/client/util/Scripting.ts65
-rw-r--r--src/client/util/ScrollBox.tsx23
-rw-r--r--src/client/util/SelectionManager.ts35
-rw-r--r--src/client/util/SettingsManager.tsx484
-rw-r--r--src/client/util/SharingManager.tsx573
-rw-r--r--src/client/util/SnappingManager.ts45
-rw-r--r--src/client/util/TrackMovements.ts270
-rw-r--r--src/client/util/UndoManager.ts51
20 files changed, 2677 insertions, 2652 deletions
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index c247afa26..0b5957fac 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -1,17 +1,17 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { convertToObject } from "typescript";
-import { Doc, DocListCast } from "../../fields/Doc";
-import { BoolCast, StrCast, Cast } from "../../fields/Types";
-import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils";
-import { LightboxView } from "../views/LightboxView";
-import { MainViewModal } from "../views/MainViewModal";
-import "./CaptureManager.scss";
-import { SelectionManager } from "./SelectionManager";
-import { undoBatch } from "./UndoManager";
-const higflyout = require("@hig/flyout");
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { convertToObject } from 'typescript';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { BoolCast, StrCast, Cast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { LightboxView } from '../views/LightboxView';
+import { MainViewModal } from '../views/MainViewModal';
+import './CaptureManager.scss';
+import { SelectionManager } from './SelectionManager';
+import { undoBatch } from './UndoManager';
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -22,32 +22,31 @@ export class CaptureManager extends React.Component<{}> {
@observable _document: any;
@observable isOpen: boolean = false; // whether the CaptureManager is to be displayed or not.
-
constructor(props: {}) {
super(props);
CaptureManager.Instance = this;
}
- public close = action(() => this.isOpen = false);
+ public close = action(() => (this.isOpen = false));
public open = action((doc: Doc) => {
this.isOpen = true;
this._document = doc;
});
-
@computed get visibilityContent() {
-
- return <div className="capture-block">
- <div className="capture-block-title">Visibility</div>
- <div className="capture-block-radio">
- <div className="radio-container">
- <input type="radio" value="private" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Private
- </div>
- <div className="radio-container">
- <input type="radio" value="public" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Public
+ return (
+ <div className="capture-block">
+ <div className="capture-block-title">Visibility</div>
+ <div className="capture-block-radio">
+ <div className="radio-container">
+ <input type="radio" value="private" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Private
+ </div>
+ <div className="radio-container">
+ <input type="radio" value="public" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Public
+ </div>
</div>
</div>
- </div>;
+ );
}
@computed get linksContent() {
@@ -66,75 +65,79 @@ export class CaptureManager extends React.Component<{}> {
order.push(
<div className="list-item">
<div className="number">{i}</div>
- {(l.anchor1 as Doc).title}
+ {StrCast((l.anchor1 as Doc).title)}
</div>
);
}
});
}
- return <div className="capture-block">
- <div className="capture-block-title">Links</div>
- <div className="capture-block-list">
- {order}
+ return (
+ <div className="capture-block">
+ <div className="capture-block-title">Links</div>
+ <div className="capture-block-list">{order}</div>
</div>
- </div>;
+ );
}
@computed get closeButtons() {
- return <div className="capture-block">
- <div className="buttons">
- <div className="save" onClick={() => {
- LightboxView.SetLightboxDoc(this._document);
- this.close();
- }}>
- Save
- </div>
- <div className="cancel" onClick={() => {
- const selected = SelectionManager.Views().slice();
- SelectionManager.DeselectAll();
- selected.map(dv => dv.props.removeDocument?.(dv.props.Document));
- this.close();
- }}>
- Cancel
+ return (
+ <div className="capture-block">
+ <div className="buttons">
+ <div
+ className="save"
+ onClick={() => {
+ LightboxView.SetLightboxDoc(this._document);
+ this.close();
+ }}>
+ Save
+ </div>
+ <div
+ className="cancel"
+ onClick={() => {
+ const selected = SelectionManager.Views().slice();
+ SelectionManager.DeselectAll();
+ selected.map(dv => dv.props.removeDocument?.(dv.props.Document));
+ this.close();
+ }}>
+ Cancel
+ </div>
</div>
</div>
- </div>;
+ );
}
-
-
private get captureInterface() {
- return <div className="capture-interface">
- <div className="capture-t1">
- <div className="recordButtonOutline" style={{}}>
- <div className="recordButtonInner" style={{}}>
+ return (
+ <div className="capture-interface">
+ <div className="capture-t1">
+ <div className="recordButtonOutline" style={{}}>
+ <div className="recordButtonInner" style={{}}></div>
</div>
+ Conversation Capture
</div>
- Conversation Capture
- </div>
- <div className="capture-t2">
-
- </div>
- {this.visibilityContent}
- {this.linksContent}
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={"times"} color="black" size={"lg"} />
+ <div className="capture-t2"></div>
+ {this.visibilityContent}
+ {this.linksContent}
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+ {this.closeButtons}
</div>
- {this.closeButtons}
- </div>;
-
+ );
}
render() {
- return <MainViewModal
- contents={this.captureInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: "500px", height: "350px", border: "none", background: "whitesmoke" }}
- overlayStyle={{ background: "black" }}
- overlayDisplayedOpacity={0.6}
- />;
+ return (
+ <MainViewModal
+ contents={this.captureInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: '500px', height: '350px', border: 'none', background: 'whitesmoke' }}
+ overlayStyle={{ background: 'black' }}
+ overlayDisplayedOpacity={0.6}
+ />
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 0db8bebd8..7856c913b 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,627 +1,374 @@
-import { computed, observable, reaction } from "mobx";
+import { reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { 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 { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { ScriptField } from "../../fields/ScriptField";
+import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
-import { SharingPermissions } from "../../fields/util";
-import { Utils } from "../../Utils";
+import { SetCachedGroups, SharingPermissions } from "../../fields/util";
+import { OmitKeys, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
-import { DocumentType } from "../documents/DocumentTypes";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
-import { TreeView } from "../views/collections/TreeView";
+import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
+import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
+import { TreeViewType } from "../views/collections/CollectionTreeView";
+import { DashboardView } from "../views/DashboardView";
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 { HistoryUtil } from "./History";
+import { MakeTemplate } from "./DropConverter";
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;
- 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; }
- @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 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
- });
- }
-
- 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) {
+ DocUtils.AssignOpts(templateBtn,btnOpts);
+ DocUtils.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions);
+ }
+ return templateBtn;
};
- linkTemplate.header = new RichTextField(JSON.stringify(rtf2), "");
+ return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts);
+ });
- 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
- });
- }
+ 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 DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs);
+ }
+
+ /// Initializes templates for editing click funcs of a document
+ static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") {
+ const tempClicks = DocCast(doc[field]);
+ const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true};
+ const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
+ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"},
+ { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}];
+ const reqdClickList = reqdTempOpts.map(opts => {
+ const allOpts = {...reqdClickOpts, ...opts.opts};
+ const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
+ return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script,allOpts));
+ });
- // 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: "child click editors", _height:75, system: true};
+ return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
}
- // 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 for editing click funcs of a document
+ static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") {
+ const tempClicks = DocCast(doc[field]);
+ const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true};
+ const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
+ { opts: { title: "onClick"}, script: "console.log( 'click')"},
+ { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"},
+ { opts: { title: "onChildClick"}, script: "console.log( 'child click')"},
+ { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"},
+ { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"},
+ ];
+ const reqdClickList = reqdTempOpts.map(opts => {
+ const allOpts = {...reqdClickOpts, ...opts.opts};
+ const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
+ return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title);
+ });
- return doc["template-notes"] as Doc;
- }
+ const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true};
+ return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
+ }
+
+ /// Initializes templates that can be applied to notes
+ static setupNoteTemplates(doc: Doc, field="template-notes") {
+ const tempNotes = DocCast(doc[field]);
+ 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", width:200, system: true};
+ const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
+ return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
+ });
- // 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 })
- }));
- }
+ const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true };
+ return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
}
- // 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;
+ /// Initializes collection of templates for notes and click functions
+ static setupDocTemplates(doc: Doc, field="myTemplates") {
+ DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"});
+ const templates = [
+ CurrentUserUtils.setupNoteTemplates(doc),
+ CurrentUserUtils.setupClickEditorTemplates(doc)
+ ];
+ CurrentUserUtils.setupChildClickEditors(doc)
+ const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, };
+ const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
+ return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts);
}
+ // 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 = DocUtils.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 DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ?
+ DocUtils.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!);
+ DocUtils.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;
- }
- 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 }
- ];
+ MakeTemplate(Doc.GetProto(header), true, "Untitled Header");
+ return header;
+ }
+ 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: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }},
+ {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 => DocUtils.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 note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard 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<string>(["_stayInCollection"]),
+ };
+ return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs);
+ });
- // 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,
- }));
-
- 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 DocUtils.AssignScripts(DocUtils.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()" },
- ];
- }
-
- 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";
- }
- }));
- });
+ { 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: Doc.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") {
+ DocUtils.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 DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
});
- return doc.menuStack as Doc;
- }
-
- // 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 DocUtils.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,};
+ DocUtils.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." },
@@ -669,42 +416,6 @@ export class CurrentUserUtils {
}) as any as Doc
- static setupThumbButtons(doc: Doc) {
- 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<string>(["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 });
@@ -723,608 +434,433 @@ 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 DocUtils.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;
- }
-
- 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"]);
- }
- return doc.myFilesystem as any as Doc;
- }
-
- 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 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 DocUtils.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 = DocUtils.AssignScripts(DocUtils.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 = DocUtils.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 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 = DocUtils.AssignScripts(DocUtils.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 = DocUtils.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;
+ }
+
+ /// initializes the left sidebar File system pane
+ static setupFilesystem(doc: Doc, field:string) {
+ var myFilesystem = DocCast(doc[field]);
+ const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true });
+
+ const newFolder = `TreeView_addNewFolder()`;
+ 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 = DocUtils.AssignScripts(DocUtils.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 = DocUtils.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;
+ }
+
+ /// 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 = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
- 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";
- }
- }
+ 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 = DocUtils.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
- 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;
+ 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;
- }
-
- // 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]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
+ return DocUtils.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 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 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}) =>
+ DocUtils.AssignScripts(DocUtils.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 DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
}
- 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 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_);}'}},
];
- 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 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 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 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 async contextMenuTools(doc: Doc) {
+ static webTools() {
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
+ { 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_); }'} },
];
}
- // 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);
- }
- }
-
- // 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 }));
- }
+ static contextMenuTools():Button[] {
+ return [
+ { 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, CollectionViewType.NoteTaking]),
+ title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
+ { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Text", icon: "text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
+ { title: "Ink", icon: "ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', inearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`} }, // Always available
+ { title: "Web", icon: "web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected
+ { title: "Schema", icon: "schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+ ];
}
- // 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
- }));
+ /// 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 DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs);
}
- // 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;
- }
- 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",]);
-
+ /// 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 = DocUtils.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 DocUtils.AssignScripts(
+ DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs);
}
- doc.mySharedDocs = new PrefetchProxy(sharedDocs);
- }
+ });
+ return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
- // 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."
- }));
- }
+ /// collection of documents rendered in the overlay layer above all tabs and other UI
+ static setupOverlays(doc: Doc, field = "myOverlayDocs") {
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true });
}
- // 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;
- }
+ static setupPublished(doc:Doc, field = "myPublishedDocs") {
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true });
}
-
- 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<string>(["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 });
+
+ /// 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);
}
- PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
+ }
- return doc.clickFuncs as Doc;
+ /// 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'"
+ };
+
+ DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
}
- 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"]),
+ /// 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 = DocUtils.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 };
+ DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ return myImports;
+ }
+ /// 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) {
+ DocUtils.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))]);
+ 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
+ DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
+ DocUtils.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();
+
+ setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
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 = DocUtils.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
+ }
+ DocUtils.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 => {
@@ -1333,7 +869,14 @@ export class CurrentUserUtils {
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(";")));
+ if (result.cacheDocumentIds)
+ {
+ const ids = result.cacheDocumentIds.split(";");
+ const batch = 10000;
+ for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) {
+ await DocServer.GetRefFields(ids.slice(i, i+batch));
+ }
+ }
return result;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?");
@@ -1342,20 +885,22 @@ export class CurrentUserUtils {
}
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") {
+ if (userDocumentId) {
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;
- 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;
+ this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
+ if (Docs.newAccount) {
+ if (Doc.CurrentUserEmail === "guest") {
+ DashboardView.createNewDashboard(undefined, "guest dashboard");
+ } else {
+ userDoc.activePage = "home";
+ }
+ }
+ return userDoc;
});
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
@@ -1363,43 +908,6 @@ export class CurrentUserUtils {
});
}
- public static _urlState: HistoryUtil.DocUrl;
-
- public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => {
- CurrentUserUtils.MainDocId = doc[Id];
-
- 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);
- }
- const state = CurrentUserUtils._urlState;
- if (state.sharing === true && !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");
@@ -1409,199 +917,36 @@ 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(Doc.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(Doc.MyImports.data, listSpec(Doc), null);
+ list?.splice(0, 0, ...results);
+ disposer();
} else {
console.log("No file selected");
}
};
input.click();
}
-
- 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 createNewDashboard = async (userDoc: Doc, id?: string) => {
- const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true);
- const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc;
- 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,
- _width: 1500,
- _height: 1000,
- _fitWidth: true,
- _backgroundGridShow: true,
- title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
- };
- const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
- const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, 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;
-
- userDoc.activePresentation = presentation;
-
- Doc.AddDocToList(dashboards, "data", dashboardDoc);
- CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
- }
-
- 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
- });
- 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 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; }
}
-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 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";
-}); \ No newline at end of file
+ScriptingGlobals.add(function MySharedDocs() { return Doc.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 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");
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 0a00ab6e0..d3ac2f03f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -4,64 +4,66 @@ import { Id } from '../../fields/FieldSymbols';
import { Cast } from '../../fields/Types';
import { returnFalse } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-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 { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { CollectionView } from '../views/collections/CollectionView';
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 }[] = [];
+ @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = [];
private static _instance: DocumentManager;
- public static get Instance(): DocumentManager { return this._instance || (this._instance = new this()); }
+ public static get Instance(): DocumentManager {
+ return this._instance || (this._instance = new this());
+ }
//private constructor so no other class can create a nodemanager
- private constructor() { }
+ private constructor() {}
@action
public AddView = (view: DocumentView) => {
//console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
- const viewAnchorIndex = view.props.LayoutTemplateString.includes("anchor2") ? "anchor2" : "anchor1";
+ const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1';
DocListCast(view.rootDoc.links).forEach(link => {
- this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).
- forEach(otherView => this.LinkedDocumentViews.push(
- {
- a: viewAnchorIndex === "anchor2" ? otherView : view,
- b: viewAnchorIndex === "anchor2" ? view : otherView,
- l: link
- })
- );
+ this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+ this.LinkedDocumentViews.push({
+ a: viewAnchorIndex === 'anchor2' ? otherView : view,
+ b: viewAnchorIndex === 'anchor2' ? view : otherView,
+ l: link,
+ })
+ );
});
this.LinkAnchorBoxViews.push(view);
// 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) => {
- this.LinkedDocumentViews.slice().forEach(action(pair => {
- if (pair.a === view || pair.b === view) {
- const li = this.LinkedDocumentViews.indexOf(pair);
- li !== -1 && this.LinkedDocumentViews.splice(li, 1);
- }
- }));
+ this.LinkedDocumentViews.slice().forEach(
+ action(pair => {
+ if (pair.a === view || pair.b === view) {
+ const li = this.LinkedDocumentViews.indexOf(pair);
+ li !== -1 && this.LinkedDocumentViews.splice(li, 1);
+ }
+ })
+ );
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
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 +71,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 +97,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,19 +123,18 @@ 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 => {
const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc);
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 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
@@ -146,17 +147,16 @@ export class DocumentManager {
return toReturn;
}
-
static addView = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, "right");
+ CollectionDockingView.AddSplit(doc, 'right');
finished?.();
- }
+ };
public jumpToDocument = async (
- targetDoc: Doc, // document to display
- willZoom: boolean, // whether to zoom doc to take up most of screen
+ 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
- linkDoc?: Doc, // link that's being followed
+ 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
finished?: () => void,
@@ -167,101 +167,158 @@ 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;
+ if (!didFocus && !wasHidden) {
+ // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
+ 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 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, {
- originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(didFocus);
- res(ViewAdjustment.doNothing);
- })
- });
+ const doFocus = (forceDidFocus: boolean) =>
+ focusView.focus(originalTarget ?? targetDoc, {
+ originalTarget,
+ willZoom,
+ scale: presZoom,
+ afterFocus: (didFocus: boolean) =>
+ new Promise<ViewAdjustment>(res => {
+ 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
+ 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..
- targetDocContext._viewTransition = "transform 500ms";
+ } 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 () => {
+ 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;
- }
+ },
});
// now find the target document within the context
- if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
+ if (targetDoc._timecodeToShow) {
+ // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
finished?.();
- } else { // no timecode means we need to find the context view and focus on our target
+ } 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, {
- willZoom, afterFocus: (didFocus: boolean) =>
+ retryDocView.focus(targetDoc, {
+ willZoom,
+ afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
- !noSelect && focusAndFinish(didFocus);
+ !noSelect && focusAndFinish(true);
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
+ 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);
+ createViewFunc(
+ targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+ finishFunc
+ );
}
}
}
- }
-
+ };
}
-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);
+ } else {
+ const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
const showDoc = context || doc;
- CollectionDockingView.AddSplit(showDoc === Doc.GetProto(showDoc) ? Doc.MakeAlias(showDoc) : showDoc, "right") && context &&
- setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc));
+ 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);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c9c499fff..947882958 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,41 +1,35 @@
-import { action } from "mobx";
-import { DateField } from "../../fields/DateField";
-import { Doc, Field, Opt } from "../../fields/Doc";
-import { List } from "../../fields/List";
-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 { Docs, DocUtils } from "../documents/Documents";
-import * as globalCssVariables from "../views/global/globalCssVariables.scss";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { SnappingManager } from "./SnappingManager";
-import { UndoManager } from "./UndoManager";
-
-export type dropActionType = "alias" | "copy" | "move" | "same" | "proto" | "none" | undefined; // undefined = move, "same" = move but don't call removeDropProperties
+import { action, observable, runInAction } from 'mobx';
+import { DateField } from '../../fields/DateField';
+import { Doc, Field, Opt } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { PrefetchProxy } from '../../fields/Proxy';
+import { listSpec } from '../../fields/Schema';
+import { SchemaHeaderField } from '../../fields/SchemaHeaderField';
+import { ScriptField } from '../../fields/ScriptField';
+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';
+import { SnappingManager } from './SnappingManager';
+import { UndoManager } from './UndoManager';
+
+export type dropActionType = 'alias' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties
/**
* Initialize drag
- * @param _reference: The HTMLElement that is being dragged
+ * @param _reference: The HTMLElement that is being dragged
* @param docFunc: The Dash document being moved
* @param moveFunc: The function called when the document is moved
* @param dropAction: What to do with the document when it is dropped
* @param dragStarted: Method to call when the drag is started
*/
-export function SetupDrag(
- _reference: React.RefObject<HTMLElement>,
- docFunc: () => Doc | Promise<Doc> | undefined,
- moveFunc?: DragManager.MoveFunction,
- dropAction?: dropActionType,
- dragStarted?: () => void
-) {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
const onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
const doc = await docFunc();
if (doc) {
@@ -47,7 +41,7 @@ export function SetupDrag(
}
};
const onRowUp = (): void => {
- document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
const onItemDown = async (e: React.PointerEvent) => {
@@ -56,15 +50,10 @@ 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);
+ document.addEventListener('pointermove', onRowMove);
+ document.addEventListener('pointerup', onRowUp);
}
}
};
@@ -74,12 +63,19 @@ 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");
+ const root = document.getElementById('root');
if (!root) {
- throw new Error("No root element found");
+ throw new Error('No root element found');
}
return root;
}
@@ -87,27 +83,20 @@ export namespace DragManager {
export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
export type RemoveFunction = (document: Doc | Doc[]) => boolean;
- export interface DragDropDisposer { (): void; }
+ export interface DragDropDisposer {
+ (): void;
+ }
export interface DragOptions {
dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed
- hideSource?: boolean; // hide source document during drag
- offsetX?: number; // offset of top left of source drag visual from cursor
+ hideSource?: boolean; // hide source document during drag
+ offsetX?: number; // offset of top left of source drag visual from cursor
offsetY?: number;
noAutoscroll?: boolean;
}
// event called when the drag operation results in a drop action
export class DropEvent {
- constructor(
- readonly x: number,
- readonly y: number,
- readonly complete: DragCompleteEvent,
- readonly shiftKey: boolean,
- readonly altKey: boolean,
- readonly metaKey: boolean,
- readonly ctrlKey: boolean,
- readonly embedKey: boolean,
- ) { }
+ constructor(readonly x: number, readonly y: number, readonly complete: DragCompleteEvent, readonly shiftKey: boolean, readonly altKey: boolean, readonly metaKey: boolean, readonly ctrlKey: boolean, readonly embedKey: boolean) {}
}
// event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated
@@ -139,20 +128,22 @@ export namespace DragManager {
treeViewDoc?: Doc;
offset: number[];
canEmbed?: boolean;
- userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
- defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action
- dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move'
+ userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
+ defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action
+ dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move'
removeDropProperties?: string[];
moveDocument?: MoveFunction;
removeDocument?: RemoveFunction;
isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts
}
export class LinkDragData {
- constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc,) {
+ constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc) {
this.linkDragView = dragView;
this.linkSourceGetAnchor = linkSourceGetAnchor;
}
- get dragDocument() { return this.linkDragView.props.Document; }
+ get dragDocument() {
+ return this.linkDragView.props.Document;
+ }
linkSourceGetAnchor: () => Doc;
linkSourceDoc?: Doc;
linkDragView: DocumentView;
@@ -179,43 +170,29 @@ export namespace DragManager {
userDropAction: dropActionType;
}
- export function MakeDropTarget(
- element: HTMLElement,
- dropFunc: (e: Event, de: DropEvent) => void,
- doc?: Doc,
- preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void,
- ): DragDropDisposer {
- if ("canDrop" in element.dataset) {
- throw new Error(
- "Element is already droppable, can't make it droppable again"
- );
+ export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer {
+ if ('canDrop' in element.dataset) {
+ throw new Error("Element is already droppable, can't make it droppable again");
}
- element.dataset.canDrop = "true";
+ element.dataset.canDrop = 'true';
const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
const preDropHandler = (e: Event) => {
const de = (e as CustomEvent<DropEvent>).detail;
preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType);
};
- element.addEventListener("dashOnDrop", handler);
- doc && element.addEventListener("dashPreDrop", preDropHandler);
+ element.addEventListener('dashOnDrop', handler);
+ doc && element.addEventListener('dashPreDrop', preDropHandler);
return () => {
- element.removeEventListener("dashOnDrop", handler);
- doc && element.removeEventListener("dashPreDrop", preDropHandler);
+ element.removeEventListener('dashOnDrop', handler);
+ doc && element.removeEventListener('dashPreDrop', preDropHandler);
delete element.dataset.canDrop;
};
}
// drag a document and drop it (or make an alias/copy on drop)
- export function StartDocumentDrag(
- eles: HTMLElement[],
- dragData: DocumentDragData,
- downX: number,
- downY: number,
- options?: DragOptions,
- dropEvent?: () => any
- ) {
+ export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) {
const addAudioTag = (dropDoc: any) => {
- dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
+ dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
return dropDoc;
};
@@ -224,16 +201,27 @@ export namespace DragManager {
dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
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));
- !["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));
- remProps.map(prop => drop[prop] = undefined);
- });
+ 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
+ )
+ );
+ !['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));
+ remProps.map(prop => (drop[prop] = undefined));
+ });
}
return e;
};
@@ -242,23 +230,22 @@ export namespace DragManager {
return true;
}
- // drag a button template and drop a new button
- export function
- StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+ // drag a button template and drop a new button
+ export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
const finishDrag = (e: DragCompleteEvent) => {
const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) });
params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields
initialize?.(bd);
- Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params);
+ Doc.GetProto(bd)['onClick-paramFieldKeys'] = new List<string>(params);
e.docDragData && (e.docDragData.droppedDocuments = [bd]);
return e;
};
options = options ?? {};
- options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate
+ options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate
StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag);
}
- // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption
+ // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption
export function StartAnchorAnnoDrag(eles: HTMLElement[], dragData: AnchorAnnoDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag(eles, dragData, downX, downY, options);
}
@@ -281,12 +268,12 @@ 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
- const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
- if (denominator === 0) return undefined; // Lines are parallel
+ const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+ if (denominator === 0) return undefined; // Lines are parallel
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
// let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
@@ -314,14 +301,14 @@ export namespace DragManager {
});
return { x: near[0], y: near[1] };
}
- // 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
+ // 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
+ const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt
const rangePts = [drag - offs[0], drag - offs[1], drag - offs[2]]; // left, mid, right or top, mid, bottom pts to try to snap to snaplines
- const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest));
+ const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => (Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest)));
const closestDists = rangePts.map((pt, i) => Math.abs(pt - closestPts[i]));
const minIndex = closestDists[0] < closestDists[1] && closestDists[0] < closestDists[2] ? 0 : closestDists[1] < closestDists[2] ? 1 : 2;
return closestDists[minIndex] < snapThreshold ? closestPts[minIndex] + offs[minIndex] : drag;
@@ -330,61 +317,67 @@ export namespace DragManager {
};
return {
x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()),
- y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines())
+ y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()),
};
}
- export let docsBeingDragged: Doc[] = [];
+ export let docsBeingDragged: Doc[] = observable([] as Doc[]);
export let CanEmbed = false;
export let DocDragData: DocumentDragData | undefined;
export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
- if (dragData.dropAction === "none") return;
+ if (dragData.dropAction === 'none') return;
DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
- const batch = UndoManager.StartBatch("dragging");
+ const batch = UndoManager.StartBatch('dragging');
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
if (!dragDiv) {
- dragDiv = document.createElement("div");
- dragDiv.className = "dragManager-dragDiv";
- dragDiv.style.pointerEvents = "none";
- dragLabel = document.createElement("div");
- dragLabel.className = "dragManager-dragLabel";
- dragLabel.style.zIndex = "100001";
- dragLabel.style.fontSize = "10px";
- dragLabel.style.position = "absolute";
- dragLabel.innerText = "drag titlebar to embed on drop"; // bcz: need to move this to a status bar
+ dragDiv = document.createElement('div');
+ dragDiv.className = 'dragManager-dragDiv';
+ dragDiv.style.pointerEvents = 'none';
+ dragLabel = document.createElement('div');
+ dragLabel.className = 'dragManager-dragLabel';
+ dragLabel.style.zIndex = '100001';
+ dragLabel.style.fontSize = '10px';
+ dragLabel.style.position = 'absolute';
+ dragLabel.innerText = 'drag titlebar to embed on drop'; // bcz: need to move this to a status bar
dragDiv.appendChild(dragLabel);
DragManager.Root().appendChild(dragDiv);
}
- Object.assign(dragDiv.style, { width: "", height: "", overflow: "" });
+ Object.assign(dragDiv.style, { width: '', height: '', overflow: '' });
dragDiv.hidden = false;
- const scaleXs: number[] = [], scaleYs: number[] = [], xs: number[] = [], ys: number[] = [];
+ const scaleXs: number[] = [],
+ scaleYs: number[] = [],
+ xs: number[] = [],
+ ys: number[] = [];
- docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const elesCont = {
- left: Number.MAX_SAFE_INTEGER, right: Number.MIN_SAFE_INTEGER,
- top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER
+ left: Number.MAX_SAFE_INTEGER,
+ right: Number.MIN_SAFE_INTEGER,
+ top: Number.MAX_SAFE_INTEGER,
+ bottom: Number.MIN_SAFE_INTEGER,
};
+ const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const dragElements = eles.map(ele => {
if (!ele.parentNode) dragDiv.appendChild(ele);
- const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement;
+ const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
const children = Array.from(dragElement.children);
- while (children.length) { // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag
+ while (children.length) {
+ // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag
const next = children.pop();
next && children.push(...Array.from(next.children));
if (next) {
- ["marker-start", "marker-mid", "marker-end"].forEach(field => {
- if (next.localName.startsWith("path") && (next.attributes as any)[field]) {
- next.setAttribute(field, (next.attributes as any)[field].value.replace("#", "#X"));
+ ['marker-start', 'marker-mid', 'marker-end'].forEach(field => {
+ if (next.localName.startsWith('path') && (next.attributes as any)[field]) {
+ next.setAttribute(field, (next.attributes as any)[field].value.replace('#', '#X'));
}
});
- if (next.localName.startsWith("marker")) {
- next.id = "X" + next.id;
+ if (next.localName.startsWith('marker')) {
+ next.id = 'X' + next.id;
}
}
}
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);
@@ -395,20 +388,31 @@ export namespace DragManager {
scaleXs.push(scaleX);
scaleYs.push(scaleY);
Object.assign(dragElement.style, {
- opacity: "0.7", position: "absolute", margin: "0", top: "0", bottom: "", left: "0", color: "black", transition: "none",
- borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex,
- transformOrigin: "0 0", width: `${rect.width / scaleX}px`, height: `${rect.height / scaleY}px`,
+ opacity: '0.7',
+ position: 'absolute',
+ margin: '0',
+ top: '0',
+ bottom: '',
+ left: '0',
+ color: 'black',
+ transition: 'none',
+ borderRadius: getComputedStyle(ele).borderRadius,
+ zIndex: globalCssVariables.contextMenuZindex,
+ transformOrigin: '0 0',
+ width: `${rect.width / scaleX}px`,
+ height: `${rect.height / scaleY}px`,
transform: `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`,
});
dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`;
- if (docsBeingDragged.length) {
- const pdfBox = dragElement.getElementsByTagName("canvas");
- const pdfBoxSrc = ele.getElementsByTagName("canvas");
- Array.from(pdfBox).filter(pb => pb.width && pb.height).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
+ if (docsToDrag.length) {
+ const pdfBox = dragElement.getElementsByTagName('canvas');
+ const pdfBoxSrc = ele.getElementsByTagName('canvas');
+ 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"));
+ [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none'));
dragDiv.appendChild(dragElement);
if (dragElement !== ele) {
@@ -425,10 +429,12 @@ export namespace DragManager {
return dragElement;
});
+ runInAction(() => docsBeingDragged.push(...docsToDrag));
+
const hideDragShowOriginalElements = (hide: boolean) => {
- dragLabel.style.display = hide ? "" : "none";
+ dragLabel.style.display = hide ? '' : 'none';
!hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.forEach(ele => ele.hidden = hide);
+ eles.forEach(ele => (ele.hidden = hide));
};
options?.hideSource && hideDragShowOriginalElements(true);
@@ -442,35 +448,41 @@ export namespace DragManager {
AbortDrag = () => {
options?.dragComplete?.(new DragCompleteEvent(true, dragData));
- cleanupDrag();
+ cleanupDrag(true);
};
- const cleanupDrag = action(() => {
+ const cleanupDrag = action((undo: boolean) => {
hideDragShowOriginalElements(false);
- document.removeEventListener("pointermove", moveHandler, true);
- document.removeEventListener("pointerup", upHandler);
+ document.removeEventListener('pointermove', moveHandler, true);
+ document.removeEventListener('pointerup', upHandler, true);
SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
- batch.end();
+ if (undo && batch.end()) UndoManager.Undo();
+ docsBeingDragged.length = 0;
});
- 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;
+ 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);
@@ -478,7 +490,7 @@ export namespace DragManager {
if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) {
const autoScrollHandler = () => {
target.dispatchEvent(
- new CustomEvent<React.DragEvent>("dashDragAutoScroll", {
+ new CustomEvent<React.DragEvent>('dashDragAutoScroll', {
bubbles: true,
detail: {
shiftKey: e.shiftKey,
@@ -487,7 +499,7 @@ export namespace DragManager {
ctrlKey: e.ctrlKey,
clientX: e.clientX,
clientY: e.clientY,
- dataTransfer: new DataTransfer,
+ dataTransfer: new DataTransfer(),
button: e.button,
buttons: e.buttons,
getModifierState: e.getModifierState,
@@ -499,8 +511,8 @@ export namespace DragManager {
screenX: e.screenX,
screenY: e.screenY,
detail: e.detail,
- view: e.view ? e.view : new Window as any,
- nativeEvent: new DragEvent("dashDragAutoScroll"),
+ view: e.view ? e.view : (new Window() as any),
+ nativeEvent: new DragEvent('dashDragAutoScroll'),
currentTarget: target,
target: target,
bubbles: true,
@@ -508,14 +520,14 @@ export namespace DragManager {
defaultPrevented: true,
eventPhase: e.eventPhase,
isTrusted: true,
- preventDefault: () => "not implemented for this event" ? false : false,
- isDefaultPrevented: () => "not implemented for this event" ? false : false,
- stopPropagation: () => "not implemented for this event" ? false : false,
- isPropagationStopped: () => "not implemented for this event" ? false : false,
+ preventDefault: () => ('not implemented for this event' ? false : false),
+ isDefaultPrevented: () => ('not implemented for this event' ? false : false),
+ stopPropagation: () => ('not implemented for this event' ? false : false),
+ isPropagationStopped: () => ('not implemented for this event' ? false : false),
persist: emptyFunction,
timeStamp: e.timeStamp,
- type: "dashDragAutoScroll"
- }
+ type: 'dashDragAutoScroll',
+ },
})
);
@@ -531,18 +543,18 @@ export namespace DragManager {
lastPt = { x, y };
dragLabel.style.transform = `translate(${xs[0] + moveVec.x + (options?.offsetX || 0)}px, ${ys[0] + moveVec.y + (options?.offsetY || 0) - 20}px)`;
- dragElements.map((dragElement, i) => (dragElement.style.transform =
- `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
- );
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`));
};
const upHandler = (e: PointerEvent) => {
- dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, cleanupDrag);
+ 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(false));
};
- document.addEventListener("pointermove", moveHandler, true);
- document.addEventListener("pointerup", upHandler);
+ document.addEventListener('pointermove', moveHandler, true);
+ document.addEventListener('pointerup', upHandler, true);
}
- async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
+ async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
const dropArgs = {
bubbles: true,
detail: {
@@ -552,13 +564,13 @@ export namespace DragManager {
altKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
- embedKey: CanEmbed
- }
+ embedKey: CanEmbed,
+ },
};
- target.dispatchEvent(new CustomEvent<DropEvent>("dashPreDrop", dropArgs));
+ target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs));
await finishDrag?.(complete);
- target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", dropArgs));
+ target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs));
options?.dragComplete?.(complete);
endDrag?.();
}
-} \ No newline at end of file
+}
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/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 62d656c5d..c8b784390 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,19 +1,20 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
import Select from 'react-select';
-import * as RequestPromise from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { StrCast, Cast } from "../../fields/Types";
-import { Utils } from "../../Utils";
-import { MainViewModal } from "../views/MainViewModal";
-import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
-import "./GroupManager.scss";
-import { GroupMemberView } from "./GroupMemberView";
-import { SharingManager, User } from "./SharingManager";
-import { listSpec } from "../../fields/Schema";
-import { DateField } from "../../fields/DateField";
+import * as RequestPromise from 'request-promise';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { StrCast, Cast } from '../../fields/Types';
+import { Utils } from '../../Utils';
+import { MainViewModal } from '../views/MainViewModal';
+import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
+import './GroupManager.scss';
+import { GroupMemberView } from './GroupMemberView';
+import { SharingManager, User } from './SharingManager';
+import { listSpec } from '../../fields/Schema';
+import { DateField } from '../../fields/DateField';
+import { Id } from '../../fields/FieldSymbols';
/**
* Interface for options for the react-select component
@@ -25,7 +26,6 @@ export interface UserOptions {
@observer
export class GroupManager extends React.Component<{}> {
-
static Instance: GroupManager;
@observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not.
@observable private users: string[] = []; // list of users populated from the database.
@@ -34,24 +34,28 @@ export class GroupManager extends React.Component<{}> {
@observable private createGroupModalOpen: boolean = false;
private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button
- @observable private buttonColour: "#979797" | "black" = "#979797";
- @observable private groupSort: "ascending" | "descending" | "none" = "none";
+ @observable private buttonColour: '#979797' | 'black' = '#979797';
+ @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none';
constructor(props: Readonly<{}>) {
super(props);
GroupManager.Instance = this;
}
- componentDidMount() { this.populateUsers(); }
+ componentDidMount() {
+ this.populateUsers();
+ }
/**
* Fetches the list of users stored on the database.
*/
populateUsers = async () => {
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
- const raw = JSON.parse(userList) as User[];
- raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
- }
+ if (Doc.UserDoc()[Id] !== '__guest__') {
+ const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
+ const raw = JSON.parse(userList) as User[];
+ raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
+ }
+ };
/**
* @returns the options to be rendered in the dropdown menu to add users and create a group.
@@ -68,11 +72,11 @@ export class GroupManager extends React.Component<{}> {
// SelectionManager.DeselectAll();
this.isOpen = true;
this.populateUsers();
- }
+ };
/**
* Hides the GroupManager.
- */
+ */
@action
close = () => {
this.isOpen = false;
@@ -81,26 +85,32 @@ export class GroupManager extends React.Component<{}> {
// this.users = [];
this.createGroupModalOpen = false;
TaskCompletionBox.taskCompleted = false;
- }
+ };
/**
* @returns the database of groups.
*/
- @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; }
+ @computed get GroupManagerDoc(): Doc | undefined {
+ return Doc.UserDoc().globalGroupDatabase as Doc;
+ }
/**
* @returns a list of all group documents.
*/
- @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); }
+ @computed get allGroups(): Doc[] {
+ return DocListCast(this.GroupManagerDoc?.data);
+ }
/**
* @returns the members of the admin group.
*/
- @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; }
+ @computed get adminGroupMembers(): string[] {
+ return this.getGroup('Admin') ? JSON.parse(StrCast(this.getGroup('Admin')!.members)) : '';
+ }
/**
* @returns a group document based on the group name.
- * @param groupName
+ * @param groupName
*/
getGroup(groupName: string): Doc | undefined {
return this.allGroups.find(group => group.title === groupName);
@@ -114,10 +124,9 @@ export class GroupManager extends React.Component<{}> {
return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
}
-
/**
* @returns a boolean indicating whether the current user has access to edit group documents.
- * @param groupDoc
+ * @param groupDoc
*/
hasEditAccess(groupDoc: Doc): boolean {
if (!groupDoc) return false;
@@ -127,12 +136,12 @@ export class GroupManager extends React.Component<{}> {
/**
* Helper method that sets up the group document.
- * @param groupName
- * @param memberEmails
+ * @param groupName
+ * @param memberEmails
*/
createGroupDoc(groupName: string, memberEmails: string[] = []) {
- const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
- const groupDoc = new Doc("GROUP:" + name, true);
+ const name = groupName.toLowerCase() === 'admin' ? 'Admin' : groupName;
+ const groupDoc = new Doc('GROUP:' + name, true);
groupDoc.title = name;
groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
groupDoc.members = JSON.stringify(memberEmails);
@@ -141,12 +150,12 @@ export class GroupManager extends React.Component<{}> {
/**
* Helper method that adds a group document to the database of group documents and @returns whether it was successfully added or not.
- * @param groupDoc
+ * @param groupDoc
*/
addGroup(groupDoc: Doc): boolean {
if (this.GroupManagerDoc) {
- Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc);
- this.GroupManagerDoc["data-lastModified"] = new DateField;
+ Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc);
+ this.GroupManagerDoc['data-lastModified'] = new DateField();
return true;
}
return false;
@@ -154,20 +163,20 @@ export class GroupManager extends React.Component<{}> {
/**
* Deletes a group from the database of group documents and @returns whether the group was deleted or not.
- * @param group
+ * @param group
*/
@action
deleteGroup(group: Doc): boolean {
if (group) {
if (this.GroupManagerDoc && this.hasEditAccess(group)) {
- Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
+ Doc.RemoveDocFromList(this.GroupManagerDoc, 'data', group);
SharingManager.Instance.removeGroup(group);
const members = JSON.parse(StrCast(group.members));
if (members.includes(Doc.CurrentUserEmail)) {
const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
}
- this.GroupManagerDoc["data-lastModified"] = new DateField;
+ this.GroupManagerDoc['data-lastModified'] = new DateField();
if (group === this.currentGroup) {
this.currentGroup = undefined;
}
@@ -179,8 +188,8 @@ export class GroupManager extends React.Component<{}> {
/**
* Adds a member to a group.
- * @param groupDoc
- * @param email
+ * @param groupDoc
+ * @param email
*/
addMemberToGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
@@ -188,14 +197,14 @@ export class GroupManager extends React.Component<{}> {
!memberList.includes(email) && memberList.push(email);
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.shareWithAddedMember(groupDoc, email);
- this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField);
+ this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField());
}
}
/**
* Removes a member from the group.
- * @param groupDoc
- * @param email
+ * @param groupDoc
+ * @param email
*/
removeMemberFromGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
@@ -205,27 +214,27 @@ export class GroupManager extends React.Component<{}> {
const user = memberList.splice(index, 1)[0];
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.removeMember(groupDoc, email);
- this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField);
+ this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField());
}
}
}
/**
* Handles changes in the users selected in the "Select users" dropdown.
- * @param selectedOptions
+ * @param selectedOptions
*/
@action
handleChange = (selectedOptions: any) => {
this.selectedUsers = selectedOptions as UserOptions[];
- }
+ };
/**
* Creates the group when the enter key has been pressed (when in the input).
- * @param e
+ * @param e
*/
handleKeyDown = (e: React.KeyboardEvent) => {
- e.key === "Enter" && this.createGroup();
- }
+ e.key === 'Enter' && this.createGroup();
+ };
/**
* Handles the input of required fields in the setup of a group and resets the relevant variables.
@@ -234,32 +243,37 @@ export class GroupManager extends React.Component<{}> {
createGroup = () => {
const { value } = this.inputRef.current!;
if (!value) {
- alert("Please enter a group name");
+ alert('Please enter a group name');
return;
}
- if (["admin", "public", "override"].includes(value.toLowerCase())) {
- if (value.toLowerCase() !== "admin" || (value.toLowerCase() === "admin" && this.getGroup("Admin"))) {
+ if (['admin', 'public', 'override'].includes(value.toLowerCase())) {
+ if (value.toLowerCase() !== 'admin' || (value.toLowerCase() === 'admin' && this.getGroup('Admin'))) {
alert(`You cannot override the ${value.charAt(0).toUpperCase() + value.slice(1)} group`);
return;
}
}
if (this.getGroup(value)) {
- alert("Please select a unique group name");
+ alert('Please select a unique group name');
return;
}
- this.createGroupDoc(value, this.selectedUsers?.map(user => user.value));
+ this.createGroupDoc(
+ value,
+ this.selectedUsers?.map(user => user.value)
+ );
this.selectedUsers = null;
- this.inputRef.current!.value = "";
- this.buttonColour = "#979797";
+ this.inputRef.current!.value = '';
+ this.buttonColour = '#979797';
const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect();
TaskCompletionBox.popupX = left - 2 * width;
TaskCompletionBox.popupY = top;
- TaskCompletionBox.textDisplayed = "Group created!";
+ TaskCompletionBox.textDisplayed = 'Group created!';
TaskCompletionBox.taskCompleted = true;
- setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000);
-
- }
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
+ };
/**
* @returns the MainViewModal which allows the user to create groups.
@@ -268,50 +282,43 @@ export class GroupManager extends React.Component<{}> {
const contents = (
<div className="group-create">
<div className="group-heading" style={{ marginBottom: 0 }}>
- <p><b>New Group</b></p>
- <div className={"close-button"} onClick={action(() => {
- this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false;
- })}>
- <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
+ <p>
+ <b>New Group</b>
+ </p>
+ <div
+ className={'close-button'}
+ onClick={action(() => {
+ this.createGroupModalOpen = false;
+ TaskCompletionBox.taskCompleted = false;
+ })}>
+ <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
</div>
</div>
- <input
- className="group-input"
- ref={this.inputRef}
- onKeyDown={this.handleKeyDown}
- autoFocus
- type="text"
- placeholder="Group name"
- onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} />
+ <input className="group-input" ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} />
<Select
isMulti
options={this.options}
onChange={this.handleChange}
- placeholder={"Select users"}
+ placeholder={'Select users'}
value={this.selectedUsers}
closeMenuOnSelect={false}
styles={{
dropdownIndicator: (base, state) => ({
...base,
transition: '0.5s all ease',
- transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined
+ transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined,
}),
- multiValue: (base) => ({
+ multiValue: base => ({
...base,
- maxWidth: "50%",
+ maxWidth: '50%',
'&:hover': {
- maxWidth: "unset"
- }
- })
+ maxWidth: 'unset',
+ },
+ }),
}}
/>
- <button
- ref={this.createGroupButtonRef}
- onClick={this.createGroup}
- style={{ background: this.buttonColour }}
- disabled={this.buttonColour === "#979797"}
- >
+ <button ref={this.createGroupButtonRef} onClick={this.createGroup} style={{ background: this.buttonColour }} disabled={this.buttonColour === '#979797'}>
Create
</button>
</div>
@@ -322,8 +329,12 @@ export class GroupManager extends React.Component<{}> {
isDisplayed={this.createGroupModalOpen}
interactive={true}
contents={contents}
- dialogueBoxStyle={{ width: "90%", height: "70%" }}
- closeOnExternalClick={action(() => { this.createGroupModalOpen = false; this.selectedUsers = null; TaskCompletionBox.taskCompleted = false; })}
+ dialogueBoxStyle={{ width: '90%', height: '70%' }}
+ closeOnExternalClick={action(() => {
+ this.createGroupModalOpen = false;
+ this.selectedUsers = null;
+ TaskCompletionBox.taskCompleted = false;
+ })}
/>
);
}
@@ -332,7 +343,6 @@ export class GroupManager extends React.Component<{}> {
* A getter that @returns the main interface for the GroupManager.
*/
private get groupInterface() {
-
const sortGroups = (d1: Doc, d2: Doc) => {
const g1 = StrCast(d1.title);
const g2 = StrCast(d2.title);
@@ -340,62 +350,50 @@ export class GroupManager extends React.Component<{}> {
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
};
- const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
+ const groups = this.groupSort === 'ascending' ? this.allGroups.sort(sortGroups) : this.groupSort === 'descending' ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
return (
<div className="group-interface">
{this.groupCreationModal}
- {this.currentGroup ?
- <GroupMemberView
- group={this.currentGroup}
- onCloseButtonClick={action(() => this.currentGroup = undefined)}
- />
- : null}
+ {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null}
<div className="group-heading">
- <p><b>Manage Groups</b></p>
- <button onClick={action(() => this.createGroupModalOpen = true)}>
- <FontAwesomeIcon icon={"plus"} size={"sm"} /> Create Group
+ <p>
+ <b>Manage Groups</b>
+ </p>
+ <button onClick={action(() => (this.createGroupModalOpen = true))}>
+ <FontAwesomeIcon icon={'plus'} size={'sm'} /> Create Group
</button>
- <div className={"close-button"} onClick={this.close}>
- <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
+ <div className={'close-button'} onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
</div>
</div>
<div className="main-container">
- <div
- className="sort-groups"
- onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
- Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} />
- : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} />
- : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />
- }
+ <div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
+ Name{' '}
+ {this.groupSort === 'ascending' ? (
+ <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
+ ) : this.groupSort === 'descending' ? (
+ <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
+ ) : (
+ <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
+ )}
</div>
<div className="group-body">
- {groups.map(group =>
- <div
- className="group-row"
- key={StrCast(group.title || group.groupName)}
- >
- <div className="group-name" >{group.title || group.groupName}</div>
- <div className="group-info" onClick={action(() => this.currentGroup = group)}>
- <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
+ {groups.map(group => (
+ <div className="group-row" key={StrCast(group.title || group.groupName)}>
+ <div className="group-name">{StrCast(group.title || group.groupName)}</div>
+ <div className="group-info" onClick={action(() => (this.currentGroup = group))}>
+ <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
</div>
</div>
- )}
+ ))}
</div>
</div>
-
</div>
);
}
render() {
- return <MainViewModal
- contents={this.groupInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- dialogueBoxStyle={{ zIndex: 1002 }}
- overlayStyle={{ zIndex: 1001 }}
- closeOnExternalClick={this.close}
- />;
+ return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index e6f75a7db..18aee6444 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,8 +1,8 @@
-import { Doc } from "../../fields/Doc";
-import { DocServer } from "../DocServer";
import * as qs from 'query-string';
-import { Utils, OmitKeys } from "../../Utils";
-import { CurrentUserUtils } from "./CurrentUserUtils";
+import { Doc } from '../../fields/Doc';
+import { OmitKeys, Utils } from '../../Utils';
+import { DocServer } from '../DocServer';
+import { DashboardView } from '../views/DashboardView';
export namespace HistoryUtil {
export interface DocInitializerList {
@@ -10,7 +10,7 @@ export namespace HistoryUtil {
}
export interface DocUrl {
- type: "doc";
+ type: 'doc';
docId: string;
initializers?: {
[docId: string]: DocInitializerList;
@@ -25,11 +25,11 @@ export namespace HistoryUtil {
// const handlers: ((state: ParsedUrl | null) => void)[] = [];
function onHistory(e: PopStateEvent) {
- if (window.location.pathname !== "/home") {
- const url = e.state as ParsedUrl || parseUrl(window.location);
+ if (window.location.pathname !== '/home') {
+ const url = (e.state as ParsedUrl) || parseUrl(window.location);
if (url) {
switch (url.type) {
- case "doc":
+ case 'doc':
onDocUrl(url);
break;
}
@@ -43,13 +43,13 @@ export namespace HistoryUtil {
let _lastStatePush = 0;
export function pushState(state: ParsedUrl) {
if (Date.now() - _lastStatePush > 1000) {
- history.pushState(state, "", createUrl(state));
+ history.pushState(state, '', createUrl(state));
}
_lastStatePush = Date.now();
}
export function replaceState(state: ParsedUrl) {
- history.replaceState(state, "", createUrl(state));
+ history.replaceState(state, '', createUrl(state));
}
function copyState(state: ParsedUrl): ParsedUrl {
@@ -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) {
@@ -76,10 +78,10 @@ export namespace HistoryUtil {
const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {};
const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {};
- type ParserValue = true | "none" | "json" | ((value: string) => any);
+ type ParserValue = true | 'none' | 'json' | ((value: string) => any);
type Parser = {
- [key: string]: ParserValue
+ [key: string]: ParserValue;
};
function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) {
@@ -88,9 +90,9 @@ export namespace HistoryUtil {
return value;
}
if (Array.isArray(value)) {
- } else if (parser === true || parser === "json") {
+ } else if (parser === true || parser === 'json') {
value = JSON.parse(value);
- } else if (parser === "none") {
+ } else if (parser === 'none') {
} else {
value = parser(value);
}
@@ -140,29 +142,28 @@ export namespace HistoryUtil {
}
const queryObj = OmitKeys(state, keys).extract;
const query: any = {};
- Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key]));
+ Object.keys(queryObj).forEach(key => (query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key])));
const queryString = qs.stringify(query);
- return path + (queryString ? `?${queryString}` : "");
+ return path + (queryString ? `?${queryString}` : '');
};
}
- addParser("doc", {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => {
+ addParser('doc', {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => {
if (pathname.length !== 2) return undefined;
current.initializers = current.initializers || {};
const docId = pathname[1];
current.docId = docId;
});
- addStringifier("doc", ["initializers", "readonly", "nro"], (state, current) => {
+ addStringifier('doc', ['initializers', 'readonly', 'nro'], (state, current) => {
return `${current}/${state.docId}`;
});
-
export function parseUrl(location: Location | URL): ParsedUrl | undefined {
const pathname = location.pathname.substring(1);
const search = location.search;
const opts = search.length ? qs.parse(search, { sort: false }) : {};
- const pathnameSplit = pathname.split("/");
+ const pathnameSplit = pathname.split('/');
const type = pathnameSplit[0];
@@ -177,7 +178,7 @@ export namespace HistoryUtil {
if (params.type in stringifiers) {
return stringifiers[params.type](params);
}
- return "";
+ return '';
}
export async function initDoc(id: string, initializer: DocInitializerList) {
@@ -195,7 +196,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);
+ DashboardView.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/LinkFollower.ts b/src/client/util/LinkFollower.ts
new file mode 100644
index 000000000..f75ac24f5
--- /dev/null
+++ b/src/client/util/LinkFollower.ts
@@ -0,0 +1,112 @@
+import { action, observable, observe } from 'mobx';
+import { computedFn } from 'mobx-utils';
+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';
+import { LightboxView } from '../views/LightboxView';
+import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocumentManager } from './DocumentManager';
+import { UndoManager } from './UndoManager';
+
+type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
+/*
+ * link doc:
+ * - anchor1: doc
+ * - anchor2: doc
+ *
+ * group doc:
+ * - type: string representing the group type/name/category
+ * - metadata: doc representing the metadata kvps
+ *
+ * metadata doc:
+ * - user defined kvps
+ */
+export class LinkFollower {
+ // follows a link - if the target is on screen, it highlights/pans to it.
+ // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
+ // depending on the followLinkLocation property of the source (or the link itself as a fallback);
+ public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
+ const batch = UndoManager.StartBatch('follow link click');
+ // open up target if it's not already in view ...
+ const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
+ const createTabForTarget = (didFocus: boolean) =>
+ new Promise<ViewAdjustment>(res => {
+ const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc);
+ docViewProps.addDocTab(doc, where);
+ setTimeout(() => {
+ const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
+ const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
+ if (targDocView) {
+ targDocView.props.focus(doc, {
+ willZoom: BoolCast(sourceDoc.followLinkZoom, false),
+ afterFocus: (didFocus: boolean) => {
+ finished?.();
+ res(ViewAdjustment.resetView);
+ return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing));
+ },
+ });
+ } else {
+ res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target
+ }
+ });
+ });
+
+ if (!sourceDoc.followLinkZoom) {
+ createTabForTarget(false);
+ } else {
+ // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target
+ docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
+ }
+ };
+ LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
+ };
+
+ public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
+ const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
+ const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
+ const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
+ const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+ 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 = 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
+ : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
+ ? linkDoc.anchor2
+ : linkDoc.anchor1
+ ) as Doc;
+ if (target) {
+ if (target.TourMap) {
+ const fieldKey = Doc.LayoutFieldKey(target);
+ const tour = DocListCast(target[fieldKey]).reverse();
+ LightboxView.SetLightboxDoc(currentContext, undefined, tour);
+ setTimeout(LightboxView.Next);
+ allFinished();
+ } else {
+ const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
+ const containerDoc = containerAnnoDoc || target;
+ 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 {
+ allFinished();
+ }
+ } else {
+ allFinished();
+ }
+ });
+ }
+}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index df2c02a8d..7a12a8580 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,123 +1,148 @@
-import { action, observable, observe } from "mobx";
-import { computedFn } from "mobx-utils";
-import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { ProxyField } from "../../fields/Proxy";
-import { BoolCast, Cast, StrCast } from "../../fields/Types";
-import { LightboxView } from "../views/LightboxView";
-import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView";
-import { DocumentManager } from "./DocumentManager";
-import { UndoManager } from "./UndoManager";
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
-/*
- * link doc:
- * - anchor1: doc
+import { action, observable, observe } from 'mobx';
+import { computedFn } from 'mobx-utils';
+import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { ProxyField } from '../../fields/Proxy';
+import { Cast, StrCast } from '../../fields/Types';
+/*
+ * link doc:
+ * - anchor1: doc
* - anchor2: doc
- *
+ *
* group doc:
* - type: string representing the group type/name/category
* - metadata: doc representing the metadata kvps
- *
+ *
* metadata doc:
- * - user defined kvps
+ * - user defined kvps
*/
export class LinkManager {
-
@observable static _instance: LinkManager;
@observable static userLinkDBs: Doc[] = [];
public static currentLink: Opt<Doc>;
- public static get Instance() { return LinkManager._instance; }
+ 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) => {
+ 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(() => {
+ })
+ );
+ });
+ };
+ 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)
+ })
+ );
+ };
+ 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":
+ 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;
+ 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);
+ },
+ 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 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;
+ case 'splice':
+ (change as any).added.forEach(watchUserLinkDB);
+ break;
+ case 'update': //let oldValue = change.oldValue;
}
- }, true);
- LinkManager.addLinkDB(Doc.LinkDBDoc());
- });
+ },
+ 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);
+ })
+ );
}
-
public createLinkrelationshipLists = () => {
//create new lists for link relations and their associated colors if the lists don't already exist
!Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>());
!Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>());
!Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>());
- }
+ };
public addLink(linkDoc: Doc, checkExists = false) {
if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
- Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc);
+ Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
}
}
- public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); }
- public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); }
+ public deleteLink(linkDoc: Doc) {
+ return Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc);
+ }
+ public deleteAllLinksOnAnchor(anchor: Doc) {
+ LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+ }
- public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor
+ public getAllRelatedLinks(anchor: Doc) {
+ return this.relatedLinker(anchor);
+ } // finds all links that contain the given anchor
public getAllDirectLinks(anchor: Doc): Doc[] {
// FIXME:glr Why is Doc undefined?
if (Doc.GetProto(anchor)[DirectLinksSym]) {
@@ -130,18 +155,16 @@ export class LinkManager {
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
const lfield = Doc.LayoutFieldKey(anchor);
if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) {
- console.log("WAITING FOR DOC/PROTO IN LINKMANAGER");
+ console.log('WAITING FOR DOC/PROTO IN LINKMANAGER');
return [];
}
const dirLinks = Doc.GetProto(anchor)[DirectLinksSym];
- const annos = DocListCast(anchor[lfield + "-annotations"]);
- const timelineAnnos = DocListCast(anchor[lfield + "-annotations-timeline"]);
+ const annos = DocListCast(anchor[lfield + '-annotations']);
+ const timelineAnnos = DocListCast(anchor[lfield + '-annotations-timeline']);
if (!annos || !timelineAnnos) {
debugger;
}
- const related = [...annos, ...timelineAnnos].reduce((list, anno) =>
- [...list, ...LinkManager.Instance.relatedLinker(anno)],
- Array.from(dirLinks).slice());
+ const related = [...annos, ...timelineAnnos].reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
return related;
}, true);
@@ -149,13 +172,15 @@ 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("*");
- anchorGroups.set("*", group ? [...group, link] : [link]);
+ const group = anchorGroups.get('*');
+ anchorGroups.set('*', group ? [...group, link] : [link]);
}
});
return anchorGroups;
@@ -173,81 +198,4 @@ export class LinkManager {
if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1;
if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
-
-
- // follows a link - if the target is on screen, it highlights/pans to it.
- // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
- // depending on the followLinkLocation property of the source (or the link itself as a fallback);
- public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
- const batch = UndoManager.StartBatch("follow link click");
- // open up target if it's not already in view ...
- const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
- const createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => {
- const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc);
- docViewProps.addDocTab(doc, where);
- setTimeout(() => {
- const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
- const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
- if (targDocView) {
- targDocView.props.focus(doc, {
- willZoom: BoolCast(sourceDoc.followLinkZoom, false),
- afterFocus: (didFocus: boolean) => {
- finished?.();
- res(ViewAdjustment.resetView);
- return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing));
- }
- });
- } else {
- res(where !== "inPlace" ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target
- }
- });
- });
-
- if (!sourceDoc.followLinkZoom) {
- createTabForTarget(false);
- } else {
- // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target
- docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
- }
- };
- LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
- }
-
- public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
- const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
- const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
- const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
- const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
- 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]]) : [];
- followLinks.forEach(async linkDoc => {
- if (linkDoc) {
- const target = (sourceDoc === linkDoc.anchor1 ? linkDoc.anchor2 : sourceDoc === linkDoc.anchor2 ? linkDoc.anchor1 :
- (Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc;
- if (target) {
- if (target.TourMap) {
- const fieldKey = Doc.LayoutFieldKey(target);
- const tour = DocListCast(target[fieldKey]).reverse();
- LightboxView.SetLightboxDoc(currentContext, undefined, tour);
- setTimeout(LightboxView.Next);
- finished?.();
- } 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);
- }
- } else {
- finished?.();
- }
- } else {
- finished?.();
- }
- });
- }
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
new file mode 100644
index 000000000..86bc4c5de
--- /dev/null
+++ b/src/client/util/ReplayMovements.ts
@@ -0,0 +1,208 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, observe, reaction } from "mobx";
+import { Doc } from "../../fields/Doc";
+import { VideoBox } from "../views/nodes/VideoBox";
+import { DocumentManager } from "./DocumentManager";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocServer } from "../DocServer";
+import { Movement, Presentation } from "./TrackMovements";
+
+export class ReplayMovements {
+ private timers: NodeJS.Timeout[] | null;
+ private videoBoxDisposeFunc: IReactionDisposer | null;
+ private videoBox: VideoBox | null;
+ private isPlaying: boolean;
+
+
+ // create static instance and getter for global use
+ @observable static _instance: ReplayMovements;
+ static get Instance(): ReplayMovements { return ReplayMovements._instance }
+ constructor() {
+ // init the global instance
+ ReplayMovements._instance = this;
+
+ // instance vars for replaying
+ this.timers = null;
+ this.videoBoxDisposeFunc = null;
+ this.videoBox = null;
+ this.isPlaying = false;
+ }
+
+ // pausing movements will dispose all timers that are planned to replay the movements
+ // play movemvents will recreate them when the user resumes the presentation
+ pauseMovements = (): undefined | Error => {
+ if (!this.isPlaying) {
+ // console.warn('[recordingApi.ts] pauseMovements(): already on paused');
+ return;
+ }
+ Doc.UserDoc().presentationMode = 'none';
+
+ this.isPlaying = false
+ // TODO: set userdoc presentMode to browsing
+ this.timers?.map(timer => clearTimeout(timer))
+ }
+
+ setVideoBox = async (videoBox: VideoBox) => {
+ // console.info('setVideoBox', videoBox);
+ if (this.videoBox !== null) { console.warn('setVideoBox on already videoBox'); }
+ if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); }
+
+
+ const { presentation } = videoBox;
+ if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; }
+
+ let docIdtoDoc: Map<string, Doc> = new Map();
+ try {
+ docIdtoDoc = await this.loadPresentation(presentation);
+ } catch {
+ console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements');
+ throw 'error loading docs from server';
+ }
+
+
+ this.videoBoxDisposeFunc =
+ reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+ ({ playing, timeViewed }) =>
+ playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
+ );
+ this.videoBox = videoBox;
+ }
+
+ removeVideoBox = () => {
+ if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+ this.videoBoxDisposeFunc();
+
+ this.videoBox = null;
+ this.videoBoxDisposeFunc = null;
+ }
+
+ // should be called from interacting with the screen
+ pauseFromInteraction = () => {
+ this.videoBox?.Pause();
+
+ this.pauseMovements();
+ }
+
+ loadPresentation = async (presentation: Presentation) => {
+ const { movements } = presentation;
+ if (movements === null) {
+ throw '[recordingApi.ts] followMovements() failed: no presentation data';
+ }
+
+ // generate a set of all unique docIds
+ const docIds = new Set<string>();
+ for (const {docId} of movements) {
+ if (!docIds.has(docId)) docIds.add(docId);
+ }
+
+ const docIdtoDoc = new Map<string, Doc>();
+
+ let refFields = await DocServer.GetRefFields([...docIds.keys()]);
+ for (const docId in refFields) {
+ if (!refFields[docId]) {
+ throw `one field was undefined`;
+ }
+ docIdtoDoc.set(docId, refFields[docId] as Doc);
+ }
+ // console.info('loadPresentation refFields', refFields, docIdtoDoc);
+
+ return docIdtoDoc;
+ }
+
+ // returns undefined if the docView isn't open on the screen
+ getCollectionFFView = (docId: string) => {
+ const isInView = DocumentManager.Instance.getDocumentViewById(docId);
+ if (isInView) { return isInView.ComponentView as CollectionFreeFormView; }
+ }
+
+ // will open the doc in a tab then return the CollectionFFView that holds it
+ openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => {
+ const doc = docIdtoDoc.get(docId);
+ if (doc == undefined) {
+ console.error(`docIdtoDoc did not contain docId ${docId}`)
+ return undefined;
+ }
+ // console.log('openTab', docId, doc);
+ CollectionDockingView.AddSplit(doc, 'right');
+ const docView = DocumentManager.Instance.getDocumentView(doc);
+ // BUG - this returns undefined if the doc is already open
+ return docView?.ComponentView as CollectionFreeFormView;
+ }
+
+ // helper to replay a movement
+ zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
+ const { panX, panY, scale } = movement;
+ scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+ document.Document._panX = panX;
+ document.Document._panY = panY;
+ }
+
+ getFirstMovements = (movements: Movement[]): Map<string, Movement> => {
+ if (movements === null) return new Map();
+ // generate a set of all unique docIds
+ const docIdtoFirstMove = new Map();
+ for (const move of movements) {
+ const { docId } = move;
+ if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
+ }
+ return docIdtoFirstMove;
+ }
+
+ endPlayingPresentation = () => {
+ this.isPlaying = false;
+ Doc.UserDoc().presentationMode = 'none';
+ }
+
+ public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
+ // console.info('playMovements', presentation, timeViewed, docIdtoDoc);
+
+ if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
+ }
+ if (this.isPlaying) return;
+
+ this.isPlaying = true;
+ Doc.UserDoc().presentationMode = 'watching';
+
+ // only get the movements that are remaining in the video time left
+ const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+ const handleFirstMovements = () => {
+ // if the first movement is a closed tab, open it
+ const firstMovement = filteredMovements[0];
+ const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined;
+ if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc);
+
+ // for the open tabs, set it to the first move
+ const docIdtoFirstMove = this.getFirstMovements(filteredMovements);
+ for (const [docId, firstMove] of docIdtoFirstMove) {
+ const colFFView = this.getCollectionFFView(docId);
+ if (colFFView) this.zoomAndPan(firstMove, colFFView);
+ }
+ }
+ handleFirstMovements();
+
+
+ // 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(() => {
+ const collectionFFView = this.getCollectionFFView(movement.docId);
+ if (collectionFFView) {
+ this.zoomAndPan(movement, collectionFFView);
+ } else {
+ // tab wasn't open - open it and play the movement
+ const openedColFFView = this.openTab(movement.docId, docIdtoDoc);
+ console.log('openedColFFView', openedColFFView);
+ openedColFFView && this.zoomAndPan(movement, openedColFFView);
+ }
+
+ // if last movement, presentation is done -> cleanup :)
+ if (movement === filteredMovements[filteredMovements.length - 1]) {
+ this.endPlayingPresentation();
+ }
+ }, timeDiff);
+ });
+ }
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 3b0a47b54..ea2bf6551 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -5,12 +5,11 @@
// import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import * as ts from "typescript";
-import { Doc, Field } from "../../fields/Doc";
-import { scriptingGlobals, ScriptingGlobals } from "./ScriptingGlobals";
+import * as ts from 'typescript';
+import { Doc, Field } from '../../fields/Doc';
+import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
-
export interface ScriptSuccess {
success: true;
result: any;
@@ -46,7 +45,6 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is
return false;
}
-
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error);
if ((options.typecheck !== false && errors.length) || !script) {
@@ -63,7 +61,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
const argsArray: any[] = [];
for (const name of customParams) {
- if (name === "this") {
+ if (name === 'this') {
continue;
}
if (name in args) {
@@ -86,11 +84,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
return { success: true, result };
} catch (error) {
-
if (batch) {
batch.end();
}
- onError?.(script + " " + error);
+ onError?.(script + ' ' + error);
return { success: false, error, result: errorVal };
}
};
@@ -151,16 +148,16 @@ class ScriptingCompilerHost {
}
export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
-export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser };
+export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser };
export type Transformer = {
- transformer: ts.TransformerFactory<ts.SourceFile>,
- getVars?: () => { capturedVariables: { [name: string]: Field } }
+ transformer: ts.TransformerFactory<ts.SourceFile>;
+ getVars?: () => { capturedVariables: { [name: string]: Field } };
};
export interface ScriptOptions {
requiredType?: string; // does function required a typed return value
- addReturn?: boolean; // does the compiler automatically add a return statement
+ addReturn?: boolean; // does the compiler automatically add a return statement
params?: { [name: string]: string }; // list of function parameters and their types
- capturedVariables?: { [name: string]: Field }; // list of captured variables
+ capturedVariables?: { [name: string]: Doc | number | string | boolean }; // list of captured variables
typecheck?: boolean; // should the compiler perform typechecking
editable?: boolean; // can the script edit Docs
traverser?: TraverserParam;
@@ -169,24 +166,28 @@ export interface ScriptOptions {
}
// function forEachNode(node:ts.Node, fn:(node:any) => void);
-function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") {
- return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => {
- forEachNode(n, onEnter, onExit, indentation + " ");
- }) || (onExit && onExit(node, indentation));
+function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = '') {
+ return (
+ onEnter(node, indentation) ||
+ ts.forEachChild(node, (n: any) => {
+ forEachNode(n, onEnter, onExit, indentation + ' ');
+ }) ||
+ (onExit && onExit(node, indentation))
+ );
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
- const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+ const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
if (options.params && !options.params.this) options.params.this = Doc.name;
if (options.params && !options.params.self) options.params.self = Doc.name;
if (options.globals) {
ScriptingGlobals.setScriptingGlobals(options.globals);
}
- const host = new ScriptingCompilerHost;
+ const host = new ScriptingCompilerHost();
if (options.traverser) {
const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
- const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
- const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined;
+ const onEnter = typeof options.traverser === 'object' ? options.traverser.onEnter : options.traverser;
+ const onLeave = typeof options.traverser === 'object' ? options.traverser.onLeave : undefined;
forEachNode(sourceFile, onEnter, onLeave);
}
if (options.transformer) {
@@ -199,17 +200,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
}
const transformed = result.transformed;
const printer = ts.createPrinter({
- newLine: ts.NewLineKind.LineFeed
+ newLine: ts.NewLineKind.LineFeed,
});
script = printer.printFile(transformed[0]);
result.dispose();
}
const paramNames: string[] = [];
- if ("this" in params || "this" in capturedVariables) {
- paramNames.push("this");
+ if ('this' in params || 'this' in capturedVariables) {
+ paramNames.push('this');
}
for (const key in params) {
- if (key === "this") continue;
+ if (key === 'this') continue;
paramNames.push(key);
}
const paramList = paramNames.map(key => {
@@ -217,21 +218,21 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
return `${key}: ${val}`;
});
for (const key in capturedVariables) {
- if (key === "this") continue;
+ if (key === 'this') continue;
const val = capturedVariables[key];
paramNames.push(key);
- paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
+ 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 paramString = paramList.join(', ');
+ const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script;
const reqTypes = requiredType ? `: ${requiredType}` : '';
const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
- host.writeFile("file.ts", funcScript);
+ host.writeFile('file.ts', funcScript);
if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
- const program = ts.createProgram(["file.ts"], {}, host);
+ const program = ts.createProgram(['file.ts'], {}, host);
const testResult = program.emit();
- const outputText = host.readFile("file.js");
+ const outputText = host.readFile('file.js');
const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx
index a209874a3..d4620ae3f 100644
--- a/src/client/util/ScrollBox.tsx
+++ b/src/client/util/ScrollBox.tsx
@@ -1,21 +1,24 @@
-import React = require("react");
+import React = require('react');
-export class ScrollBox extends React.Component {
+export class ScrollBox extends React.Component<React.PropsWithChildren<{}>> {
onWheel = (e: React.WheelEvent) => {
- if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { // If the element has a scroll bar, then we don't want the containing collection to zoom
+ if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+ // If the element has a scroll bar, then we don't want the containing collection to zoom
e.stopPropagation();
}
- }
+ };
render() {
return (
- <div style={{
- overflow: "auto",
- width: "100%",
- height: "100%",
- }} onWheel={this.onWheel}>
+ <div
+ style={{
+ overflow: 'auto',
+ width: '100%',
+ height: '100%',
+ }}
+ onWheel={this.onWheel}>
{this.props.children}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index b71086561..1c84af94a 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,15 +1,13 @@
-import { action, observable, ObservableMap } from "mobx";
-import { computedFn } from "mobx-utils";
-import { Doc, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { CollectionViewType } from "../views/collections/CollectionView";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { CurrentUserUtils } from "./CurrentUserUtils";
+import { action, observable, ObservableMap } from 'mobx';
+import { computedFn } from 'mobx-utils';
+import { Doc, Opt } from '../../fields/Doc';
+import { DocCast } from '../../fields/Types';
+import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
export namespace SelectionManager {
-
class Manager {
-
@observable IsDragging: boolean = false;
SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
@observable SelectedSchemaDocument: Doc | undefined;
@@ -63,16 +61,21 @@ export namespace SelectionManager {
manager.SelectSchemaViewDoc(document);
}
- const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
+ const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) {
+ // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
return manager.SelectedViews.get(doc) ? true : false;
});
// computed functions, such as used in IsSelected generate errors if they're called outside of a
// reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature
// to avoid unnecessary mobx invalidations when running inside a reaction.
export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean {
- return !doc ? false : outsideReaction ?
- manager.SelectedViews.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
- IsSelectedCache(doc);
+ return !doc
+ ? false
+ : outsideReaction
+ ? manager.SelectedViews.get(doc)
+ ? true
+ : false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
+ : IsSelectedCache(doc);
}
export function DeselectAll(except?: Doc): void {
@@ -96,4 +99,8 @@ export namespace SelectionManager {
export function Docs(): Doc[] {
return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
}
-} \ No newline at end of file
+}
+ScriptingGlobals.add(function SelectionManager_selectedDocType(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;
+});
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 6a26dfdc7..cf143c5e8 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,27 +1,29 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { ColorState, SketchPicker } from "react-color";
-import { Doc } from "../../fields/Doc";
-import { BoolCast, StrCast, Cast } from "../../fields/Types";
-import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils";
-import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager";
-import { DocServer } from "../DocServer";
-import { Networking } from "../Network";
-import { MainViewModal } from "../views/MainViewModal";
-import { CurrentUserUtils } from "./CurrentUserUtils";
-import { GroupManager } from "./GroupManager";
-import "./SettingsManager.scss";
-import { undoBatch } from "./UndoManager";
-const higflyout = require("@hig/flyout");
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+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 { DragManager } from './DragManager';
+import { GroupManager } from './GroupManager';
+import './SettingsManager.scss';
+import { undoBatch } from './UndoManager';
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
export enum ColorScheme {
- Dark = "-Dark",
- Light = "-Light",
- System = "-MatchSystem"
+ Dark = '-Dark',
+ Light = '-Light',
+ System = '-MatchSystem',
}
@observer
@@ -29,295 +31,363 @@ export class SettingsManager extends React.Component<{}> {
public static Instance: SettingsManager;
static _settingsStyle = addStyleSheet();
@observable private isOpen = false;
- @observable private passwordResultText = "";
+ @observable private passwordResultText = '';
@observable private playgroundMode = false;
- @observable private curr_password = "";
- @observable private new_password = "";
- @observable private new_confirm = "";
- @observable activeTab = "Accounts";
+ @observable private curr_password = '';
+ @observable private new_password = '';
+ @observable private new_confirm = '';
+ @observable activeTab = 'Accounts';
- @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; }
- @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard.colorScheme; }
+ @observable public static propertiesWidth: number = 0;
+ @observable public static headerBarHeight: number = 0;
+
+ @computed get backgroundColor() {
+ return Doc.UserDoc().activeCollectionBackground;
+ }
+ @computed get colorScheme() {
+ return Doc.ActiveDashboard?.colorScheme;
+ }
constructor(props: {}) {
super(props);
SettingsManager.Instance = this;
}
- public close = action(() => this.isOpen = false);
- public open = action(() => this.isOpen = true);
+ public close = action(() => (this.isOpen = false));
+ public open = action(() => (this.isOpen = true));
private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
private changePassword = async () => {
if (!(this.curr_password && this.new_password && this.new_confirm)) {
- runInAction(() => this.passwordResultText = "Error: Hey, we're missing some fields!");
+ runInAction(() => (this.passwordResultText = "Error: Hey, we're missing some fields!"));
} else {
const passwordBundle = { curr_pass: this.curr_password, new_pass: this.new_password, new_confirm: this.new_confirm };
const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
- runInAction(() => this.passwordResultText = error ? "Error: " + error[0].msg + "..." : "Password successfully updated!");
+ runInAction(() => (this.passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!'));
}
- }
-
- @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.UserDoc().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 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(() => {
this.playgroundMode = !this.playgroundMode;
if (this.playgroundMode) {
DocServer.Control.makeReadOnly();
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
- }
- else DocServer.Control.makeEditable();
+ addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
+ } else Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable();
});
@undoBatch
@action
changeColorScheme = action((e: React.ChangeEvent) => {
+ const activeDashboard = Doc.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)
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" });
+ 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;
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" });
+ activeDashboard.colorScheme = ColorScheme.Dark;
+ addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: 'black !important' });
break;
- case ColorScheme.System: default:
+ 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;
}
});
-
@computed get colorsContent() {
- const colorBox = (func: (color: ColorState) => void) => <SketchPicker onChange={func} color={StrCast(this.backgroundColor)}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
- '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
- '#FFFFFF', '#f1efeb', 'transparent']} />;
-
- const colorFlyout = <div className="colorFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}>
- <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} >
- <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
- </div>
- </Flyout>
- </div>;
- const userColorFlyout = <div className="colorFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}>
- <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} >
- <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
- </div>
- </Flyout>
- </div>;
+ const colorBox = (func: (color: ColorState) => void) => (
+ <SketchPicker
+ onChange={func}
+ color={StrCast(this.backgroundColor)}
+ presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
+ />
+ );
+
+ const colorFlyout = (
+ <div className="colorFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}>
+ <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}>
+ <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
+ </div>
+ </Flyout>
+ </div>
+ );
+ const userColorFlyout = (
+ <div className="colorFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}>
+ <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}>
+ <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
+ </div>
+ </Flyout>
+ </div>
+ );
const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System];
- const schemeMap = ["Light", "Dark", "Match system"];
+ const schemeMap = ['Light', 'Dark', 'Match system'];
- return <div className="colors-content">
- <div className="preferences-color">
- <div className="preferences-color-text">Background Color</div>
- {colorFlyout}
- </div>
- <div className="preferences-color">
- <div className="preferences-color-text">Border/Header Color</div>
- {userColorFlyout}
- </div>
- <div className="preferences-colorScheme">
- <div className="preferences-color-text">Color Scheme</div>
- <div className="preferences-color-controls">
- <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}>
- {colorSchemes.map((scheme, i) => <option key={scheme} value={scheme}> {schemeMap[i]} </option>)}
- </select>
+ return (
+ <div className="colors-content">
+ <div className="preferences-color">
+ <div className="preferences-color-text">Background Color</div>
+ {colorFlyout}
+ </div>
+ <div className="preferences-color">
+ <div className="preferences-color-text">Border/Header Color</div>
+ {userColorFlyout}
+ </div>
+ <div className="preferences-colorScheme">
+ <div className="preferences-color-text">Color Scheme</div>
+ <div className="preferences-color-controls">
+ <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(Doc.ActiveDashboard?.colorScheme)}>
+ {colorSchemes.map((scheme, i) => (
+ <option key={scheme} value={scheme}>
+ {' '}
+ {schemeMap[i]}{' '}
+ </option>
+ ))}
+ </select>
+ </div>
</div>
</div>
- </div>;
+ );
}
@computed get formatsContent() {
- return <div className="prefs-content">
- <div>
- <input type="checkbox" onChange={e => Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : "creationDate"} checked={Doc.UserDoc().showTitle !== undefined} />
- <div className="preferences-check">Show doc header</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => Doc.UserDoc()["documentLinksButton-fullMenu"] = !Doc.UserDoc()["documentLinksButton-fullMenu"]}
- checked={BoolCast(Doc.UserDoc()["documentLinksButton-fullMenu"])} />
- <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)} />
- <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>
+ return (
+ <div className="prefs-content">
+ <div>
+ <input type="checkbox" onChange={e => (Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().showTitle !== undefined} />
+ <div className="preferences-check">Show doc header</div>
+ </div>
+ <div>
+ <input type="checkbox" onChange={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} checked={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} />
+ <div className="preferences-check">Show full toolbar</div>
+ </div>
+ <div>
+ <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 => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} />
+ <div className="preferences-check">Show button labels</div>
+ </div>
</div>
- </div>;
+ );
}
@computed get appearanceContent() {
-
- return <div className="tab-content appearances-content">
- <div className="tab-column">
- <div className="tab-column-title">Colors</div>
- <div className="tab-column-content">{this.colorsContent}</div>
- </div>
- <div className="tab-column">
- <div className="tab-column-title">Formats</div>
- <div className="tab-column-content">{this.formatsContent}</div>
+ return (
+ <div className="tab-content appearances-content">
+ <div className="tab-column">
+ <div className="tab-column-title">Colors</div>
+ <div className="tab-column-content">{this.colorsContent}</div>
+ </div>
+ <div className="tab-column">
+ <div className="tab-column-title">Formats</div>
+ <div className="tab-column-content">{this.formatsContent}</div>
+ </div>
</div>
- </div>;
+ );
}
@computed get textContent() {
-
- const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Roboto"];
- const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"];
+ const fontFamilies = ['Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Roboto'];
+ const fontSizes = ['7px', '8px', '9px', '10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '48px', '72px'];
return (
<div className="tab-content appearances-content">
<div className="preferences-font">
<div className="preferences-font-text">Default Font</div>
<div className="preferences-font-controls">
- <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7px")}>
- {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)}
+ <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, '7px')}>
+ {fontSizes.map(size => (
+ <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}>
+ {' '}
+ {size}{' '}
+ </option>
+ ))}
</select>
- <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, "Times New Roman")} >
- {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)}
+ <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, 'Times New Roman')}>
+ {fontFamilies.map(font => (
+ <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}>
+ {' '}
+ {font}{' '}
+ </option>
+ ))}
</select>
</div>
</div>
- </div>);
+ </div>
+ );
}
@action
changeVal = (e: React.ChangeEvent, pass: string) => {
const value = (e.target as any).value;
switch (pass) {
- case "curr": this.curr_password = value; break;
- case "new": this.new_password = value; break;
- case "conf": this.new_confirm = value; break;
+ case 'curr':
+ this.curr_password = value;
+ break;
+ case 'new':
+ this.new_password = value;
+ break;
+ case 'conf':
+ this.new_confirm = value;
+ break;
}
- }
+ };
@computed get passwordContent() {
- return <div className="password-content">
- <div className="password-content-inputs">
- <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, "curr")} />
- <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, "new")} />
- <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, "conf")} />
- </div>
- <div className="password-content-buttons">
- {!this.passwordResultText ? (null) : <div className={`${this.passwordResultText.startsWith("Error") ? "error" : "success"}-text`}>{this.passwordResultText}</div>}
- <a className="password-forgot" href="/forgotPassword">forgot password?</a>
- <button className="password-submit" onClick={this.changePassword}>submit</button>
+ return (
+ <div className="password-content">
+ <div className="password-content-inputs">
+ <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, 'curr')} />
+ <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, 'new')} />
+ <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, 'conf')} />
+ </div>
+ <div className="password-content-buttons">
+ {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
+ <a className="password-forgot" href="/forgotPassword">
+ forgot password?
+ </a>
+ <button className="password-submit" onClick={this.changePassword}>
+ submit
+ </button>
+ </div>
</div>
- </div>;
+ );
}
@computed get accountOthersContent() {
- return <div className="account-others-content">
- <button onClick={this.googleAuthorize} value="data">Authorize Google Acc</button>
- </div>;
+ return (
+ <div className="account-others-content">
+ <button onClick={this.googleAuthorize} value="data">
+ Authorize Google Acc
+ </button>
+ </div>
+ );
}
@computed get accountsContent() {
- return <div className="tab-content accounts-content">
- <div className="tab-column">
- <div className="tab-column-title">Password</div>
- <div className="tab-column-content">{this.passwordContent}</div>
- </div>
- <div className="tab-column">
- <div className="tab-column-title">Others</div>
- <div className="tab-column-content">{this.accountOthersContent}</div>
+ return (
+ <div className="tab-content accounts-content">
+ <div className="tab-column">
+ <div className="tab-column-title">Password</div>
+ <div className="tab-column-content">{this.passwordContent}</div>
+ </div>
+ <div className="tab-column">
+ <div className="tab-column-title">Others</div>
+ <div className="tab-column-content">{this.accountOthersContent}</div>
+ </div>
</div>
- </div>;
+ );
}
@computed get modesContent() {
- return <div className="tab-content modes-content">
- <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"}>
- <option key={"Novice"} value={"Novice"}> Novice </option>
- <option key={"Developer"} value={"Developer"}> Developer</option>
- </select>
- <div className="modes-playground">
- <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} />
- <div className="playground-text">Playground Mode</div>
+ return (
+ <div className="tab-content modes-content">
+ <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.noviceMode ? 'Novice' : 'Developer'}>
+ <option key={'Novice'} value={'Novice'}>
+ {' '}
+ Novice{' '}
+ </option>
+ <option key={'Developer'} value={'Developer'}>
+ {' '}
+ Developer
+ </option>
+ </select>
+ <div className="modes-playground">
+ <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} />
+ <div className="playground-text">Playground Mode</div>
+ </div>
</div>
</div>
- </div>
- <div className="tab-column">
- <div className="tab-column-title">Permissions</div>
- <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)} />
- <div className="acl-text">Default access private</div>
+ <div className="tab-column">
+ <div className="tab-column-title">Permissions</div>
+ <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.defaultAclPrivate)} onChange={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} />
+ <div className="acl-text">Default access private</div>
+ </div>
</div>
</div>
</div>
-
- </div>;
+ );
}
-
private get settingsInterface() {
// const pairs = [{ title: "Password", ele: this.passwordContent }, { title: "Modes", ele: this.modesContent },
// { title: "Accounts", ele: this.accountsContent }, { title: "Preferences", ele: this.preferencesContent }];
- const tabs = [{ title: "Accounts", ele: this.accountsContent }, { title: "Modes", ele: this.modesContent },
- { title: "Appearance", ele: this.appearanceContent }, { title: "Text", ele: this.textContent }];
+ const tabs = [
+ { title: 'Accounts', ele: this.accountsContent },
+ { title: 'Modes', ele: this.modesContent },
+ { title: 'Appearance', ele: this.appearanceContent },
+ { title: 'Text', ele: this.textContent },
+ ];
- return <div className="settings-interface">
- <div className="settings-panel">
- <div className="settings-tabs">
- {tabs.map(tab => <div key={tab.title} className={"tab-control " + (this.activeTab === tab.title ? "active" : "inactive")} onClick={action(() => this.activeTab = tab.title)}>{tab.title}</div>)}
- </div>
+ return (
+ <div className="settings-interface">
+ <div className="settings-panel">
+ <div className="settings-tabs">
+ {tabs.map(tab => (
+ <div key={tab.title} className={'tab-control ' + (this.activeTab === tab.title ? 'active' : 'inactive')} onClick={action(() => (this.activeTab = tab.title))}>
+ {tab.title}
+ </div>
+ ))}
+ </div>
- <div className="settings-user">
- <div className="settings-username">{Doc.CurrentUserEmail}</div>
- <button className="logout-button" onClick={() => window.location.assign(Utils.prepend("/logout"))} >
- {CurrentUserUtils.GuestDashboard ? "Exit" : "Log Out"}
- </button>
+ <div className="settings-user">
+ <div className="settings-username">{Doc.CurrentUserEmail}</div>
+ <button className="logout-button" onClick={() => window.location.assign(Utils.prepend('/logout'))}>
+ {Doc.GuestDashboard ? 'Exit' : 'Log Out'}
+ </button>
+ </div>
</div>
- </div>
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={"times"} color="black" size={"lg"} />
- </div>
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
- <div className="settings-content">
- {tabs.map(tab => <div key={tab.title} className={"tab-section " + (this.activeTab === tab.title ? "active" : "inactive")}>{tab.ele}</div>)}
+ <div className="settings-content">
+ {tabs.map(tab => (
+ <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}>
+ {tab.ele}
+ </div>
+ ))}
+ </div>
</div>
- </div>;
-
+ );
}
render() {
- return <MainViewModal
- contents={this.settingsInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: "500px", height: "300px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />;
+ return (
+ <MainViewModal
+ contents={this.settingsInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ />
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 6d7f7e8df..895bd3374 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,29 +1,29 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { intersection } from "lodash";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import Select from "react-select";
-import * as RequestPromise from "request-promise";
-import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util";
-import { Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
-import { CollectionView } from "../views/collections/CollectionView";
-import { DictationOverlay } from "../views/DictationOverlay";
-import { MainViewModal } from "../views/MainViewModal";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
-import { SearchBox } from "../views/search/SearchBox";
-import { CurrentUserUtils } from "./CurrentUserUtils";
-import { DocumentManager } from "./DocumentManager";
-import { GroupManager, UserOptions } from "./GroupManager";
-import { GroupMemberView } from "./GroupMemberView";
-import { SelectionManager } from "./SelectionManager";
-import "./SharingManager.scss";
-import { LinkManager } from "./LinkManager";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { intersection } from 'lodash';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import Select from 'react-select';
+import * as RequestPromise from 'request-promise';
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
+import { Utils } from '../../Utils';
+import { DocServer } from '../DocServer';
+import { CollectionView } from '../views/collections/CollectionView';
+import { DictationOverlay } from '../views/DictationOverlay';
+import { MainViewModal } from '../views/MainViewModal';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
+import { SearchBox } from '../views/search/SearchBox';
+import { DocumentManager } from './DocumentManager';
+import { GroupManager, UserOptions } from './GroupManager';
+import { GroupMemberView } from './GroupMemberView';
+import { SelectionManager } from './SelectionManager';
+import './SharingManager.scss';
+import { LinkManager } from './LinkManager';
+import { Id } from '../../fields/FieldSymbols';
export interface User {
email: string;
@@ -44,19 +44,19 @@ interface GroupedOptions {
// const DefaultColor = "black";
// used to differentiate between individuals and groups when sharing
-const indType = "!indType/";
-const groupType = "!groupType/";
+const indType = '!indType/';
+const groupType = '!groupType/';
-const storage = "data";
+const storage = 'data';
/**
* A user who also has a sharing doc.
*/
interface ValidatedUser {
- user: User; // database minimal info to identify / communicate with a user (email, sharing doc id)
- sharingDoc: Doc; // document to share/message another user
+ user: User; // database minimal info to identify / communicate with a user (email, sharing doc id)
+ sharingDoc: Doc; // document to share/message another user
linkDatabase: Doc;
- userColor: string; // stored on the sharinDoc, extracted for convenience?
+ userColor: string; // stored on the sharinDoc, extracted for convenience?
}
@observer
@@ -71,8 +71,8 @@ export class SharingManager extends React.Component<{}> {
@observable private overlayOpacity = 0.4; // for the modal
@observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with
@observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users
- @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals
- @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups
+ @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals
+ @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups
private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup
// if both showUserOptions and showGroupOptions are false then both are displayed
@@ -89,7 +89,7 @@ export class SharingManager extends React.Component<{}> {
[AclAugment, SharingPermissions.Augment],
[AclSelfEdit, SharingPermissions.SelfEdit],
[AclEdit, SharingPermissions.Edit],
- [AclAdmin, SharingPermissions.Admin]
+ [AclAdmin, SharingPermissions.Admin],
]);
// private get linkVisible() {
@@ -105,17 +105,20 @@ export class SharingManager extends React.Component<{}> {
this.isOpen = this.targetDoc !== undefined;
this.permissions = SharingPermissions.Augment;
});
- }
+ };
public close = action(() => {
this.isOpen = false;
this.selectedUsers = null; // resets the list of users and selected users (in the react-select component)
TaskCompletionBox.taskCompleted = false;
- setTimeout(action(() => {
- // this.copied = false;
- DictationOverlay.Instance.hasActiveModal = false;
- this.targetDoc = undefined;
- }), 500);
+ setTimeout(
+ action(() => {
+ // this.copied = false;
+ DictationOverlay.Instance.hasActiveModal = false;
+ this.targetDoc = undefined;
+ }),
+ 500
+ );
});
constructor(props: {}) {
@@ -134,9 +137,9 @@ export class SharingManager extends React.Component<{}> {
* Populates the list of validated users (this.users) by adding registered users which have a sharingDocument.
*/
populateUsers = async () => {
- if (!this.populating) {
+ if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') {
this.populating = true;
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
const raw = JSON.parse(userList) as User[];
const sharingDocs: ValidatedUser[] = [];
const evaluating = raw.map(async user => {
@@ -146,7 +149,8 @@ export class SharingManager extends React.Component<{}> {
const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
await DocListCastAsync(linkDatabase.data);
- (await DocListCastAsync(Cast(linkDatabase, Doc, null).data))?.forEach(async link => { // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager
+ (await DocListCastAsync(Cast(linkDatabase, Doc, null).data))?.forEach(async link => {
+ // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager
const a1 = await Cast(link?.anchor1, Doc, null);
const a2 = await Cast(link?.anchor2, Doc, null);
});
@@ -166,7 +170,7 @@ export class SharingManager extends React.Component<{}> {
this.populating = false;
});
}
- }
+ };
/**
* Shares the document with a user.
@@ -176,74 +180,77 @@ export class SharingManager extends React.Component<{}> {
const target = targetDoc || this.targetDoc!;
const acl = `acl-${normalizeEmail(user.email)}`;
const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
- const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
+ const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
- }
- else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
- }
+ return !docs
+ .map(doc => {
+ doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
+
+ if (permission === SharingPermissions.None) {
+ if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
+ } else {
+ if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
+ }
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
- else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
- }).some(success => !success);
- }
+ this.setDashboardBackground(doc, permission as SharingPermissions);
+ if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
+ else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc);
+ })
+ .some(success => !success);
+ };
/**
* Sets the permission on the target for the group.
- * @param group
- * @param permission
+ * @param group
+ * @param permission
*/
setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
-
const target = targetDoc || this.targetDoc!;
const key = normalizeEmail(StrCast(group.title));
const acl = `acl-${key}`;
- const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
+ const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
// ! ensures it returns true if document has been shared successfully, false otherwise
- return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
+ return !docs
+ .map(doc => {
+ doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
+
+ if (permission === SharingPermissions.None) {
+ if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
+ } else {
+ if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
+ }
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
- }
- else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
- }
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
+ if (group instanceof Doc) {
+ const members: string[] = JSON.parse(StrCast(group.members));
+ const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
- if (group instanceof Doc) {
- const members: string[] = JSON.parse(StrCast(group.members));
- const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
-
- // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
- group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]);
+ // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
+ group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : (group.docsShared = new List<Doc>([doc]));
- return users.map(({ user, sharingDoc }) => {
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
- else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
- }).some(success => !success);
- }
- }).some(success => success);
- }
+ return users
+ .map(({ user, sharingDoc }) => {
+ if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+ else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc); // remove the doc from the list if it already exists
+ })
+ .some(success => !success);
+ }
+ })
+ .some(success => success);
+ };
/**
* Shares the documents shared with a group with a new user who has been added to that group.
- * @param group
- * @param emailId
+ * @param group
+ * @param emailId
*/
shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => {
const user = this.users.find(({ user: { email } }) => email === emailId)!;
@@ -255,54 +262,53 @@ export class SharingManager extends React.Component<{}> {
DocListCastAsync(group.docsShared).then(dl => {
const filtered = dl?.filter(doc => !userdocs?.includes(doc));
filtered && userdocs?.push(...filtered);
- }));
+ })
+ );
}
}
- }
+ };
/**
* Called from the properties sidebar to change permissions of a user.
*/
shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => {
- if (shareWith !== "Public" && shareWith !== "Override") {
- const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith));
+ if (shareWith !== 'Public' && shareWith !== 'Override') {
+ const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith));
docs.forEach(doc => {
if (user) this.setInternalSharing(user, permission, doc);
else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc);
});
- }
- else {
- const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
+ } else {
+ const dashboards = DocListCast(Doc.MyDashboards.data);
docs.forEach(doc => {
const isDashboard = dashboards.indexOf(doc) !== -1;
if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
});
}
- }
+ };
/**
* Sets the background of the Dashboard if it has been shared as a visual indicator
*/
setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
- if (Doc.IndexOf(doc, DocListCast(CurrentUserUtils.MyDashboards.data)) !== -1) {
+ if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
if (permission !== SharingPermissions.None) {
doc.isShared = true;
- doc.backgroundColor = "green";
- }
- else {
+ doc.backgroundColor = 'green';
+ } else {
const acls = doc[DataSym][AclSym];
- if (Object.keys(acls).every(key => key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key]))) {
+ if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) {
doc.isShared = undefined;
doc.backgroundColor = undefined;
}
}
}
- }
+ };
/**
* Removes the documents shared with a user through a group when the user is removed from the group.
- * @param group
- * @param emailId
+ * @param group
+ * @param emailId
*/
removeMember = (group: Doc, emailId: string) => {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
@@ -315,15 +321,15 @@ export class SharingManager extends React.Component<{}> {
})
);
}
- }
+ };
/**
* Removes a group's permissions from documents that have been shared with it.
- * @param group
+ * @param group
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
- const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
+ const dashboards = DocListCast(Doc.MyDashboards.data);
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
const isDashboard = dashboards.indexOf(doc) !== -1;
@@ -335,9 +341,7 @@ export class SharingManager extends React.Component<{}> {
users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc));
});
}
- }
-
-
+ };
// private setExternalSharing = (permission: string) => {
// const targetDoc = this.targetDoc;
@@ -367,28 +371,28 @@ export class SharingManager extends React.Component<{}> {
*/
private sharingOptions(uniform: boolean, override?: boolean) {
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 =>
- (
+ if (!uniform) dropdownValues.unshift('-multiple-');
+ if (override) dropdownValues.unshift('None');
+ return dropdownValues
+ .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any))
+ .map(permission => (
<option key={permission} value={permission}>
{permission}
</option>
- )
- );
+ ));
}
private focusOn = (contents: string) => {
- const title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
+ const title = this.targetDoc ? StrCast(this.targetDoc.title) : '';
const docs = SelectionManager.Views().length > 1 ? SelectionManager.Views().map(docView => docView.props.Document) : [this.targetDoc];
return (
<span
- className={"focus-span"}
+ className={'focus-span'}
title={title}
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(() => {
@@ -404,12 +408,11 @@ export class SharingManager extends React.Component<{}> {
this.dialogueBoxOpacity = 1;
this.overlayOpacity = 0.4;
}
- })}
- >
+ })}>
{contents}
</span>
);
- }
+ };
/**
* Handles changes in the users selected in react-select
@@ -417,7 +420,7 @@ export class SharingManager extends React.Component<{}> {
@action
handleUsersChange = (selectedOptions: any) => {
this.selectedUsers = selectedOptions as UserOptions[];
- }
+ };
/**
* Handles changes in the permission chosen to share with someone with
@@ -425,7 +428,7 @@ export class SharingManager extends React.Component<{}> {
@action
handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
this.permissions = event.currentTarget.value as SharingPermissions;
- }
+ };
/**
* Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
@@ -436,8 +439,7 @@ export class SharingManager extends React.Component<{}> {
this.selectedUsers.forEach(user => {
if (user.value.includes(indType)) {
this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
- }
- else {
+ } else {
this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
}
});
@@ -445,13 +447,16 @@ export class SharingManager extends React.Component<{}> {
const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
TaskCompletionBox.popupX = left - 1.5 * width;
TaskCompletionBox.popupY = top - 1.5 * height;
- TaskCompletionBox.textDisplayed = "Document shared!";
+ TaskCompletionBox.textDisplayed = 'Document shared!';
TaskCompletionBox.taskCompleted = true;
- setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000);
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
this.selectedUsers = null;
}
- }
+ };
// distributeOverCollection = (targetDoc?: Doc) => {
// const target = targetDoc || this.targetDoc!;
@@ -471,7 +476,7 @@ export class SharingManager extends React.Component<{}> {
const { email: e1 } = u1.user;
const { email: e2 } = u2.user;
return e1 < e2 ? -1 : e1 === e2 ? 0 : 1;
- }
+ };
/**
* Sorting algorithm to sort groups.
@@ -480,7 +485,7 @@ export class SharingManager extends React.Component<{}> {
const g1 = StrCast(group1.title);
const g2 = StrCast(group2.title);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
- }
+ };
/**
* @returns the main interface of the SharingManager.
@@ -488,28 +493,29 @@ export class SharingManager extends React.Component<{}> {
@computed get sharingInterface() {
TraceMobx();
const groupList = GroupManager.Instance?.allGroups || [];
- const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email }));
- const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
+ const sortedUsers = this.users
+ .slice()
+ .sort(this.sortUsers)
+ .map(({ user: { email } }) => ({ label: email, value: indType + email }));
+ const sortedGroups = groupList
+ .slice()
+ .sort(this.sortGroups)
+ .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
// the next block handles the users shown (individuals/groups/both)
const options: GroupedOptions[] = [];
if (GroupManager.Instance) {
if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) {
- options.push(
- { label: 'Individuals', options: sortedUsers },
- { label: 'Groups', options: sortedGroups });
- }
- else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers });
+ options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups });
+ } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers });
else options.push({ label: 'Groups', options: sortedGroups });
}
- const users = this.individualSort === "ascending" ? this.users.slice().sort(this.sortUsers) : this.individualSort === "descending" ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
- const groups = this.groupSort === "ascending" ? groupList.slice().sort(this.sortGroups) : this.groupSort === "descending" ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
+ const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
+ const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
// handles the case where multiple documents are selected
- let docs = SelectionManager.Views().length < 2 ?
- [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]]
- : SelectionManager.Views().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]);
+ let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]));
if (this.myDocAcls) {
const newDocs: Doc[] = [];
@@ -524,209 +530,174 @@ export class SharingManager extends React.Component<{}> {
const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
// users in common between all docs
- const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])));
+ const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))));
// the list of users shared with
- const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => {
- const userKey = `acl-${normalizeEmail(user.email)}`;
- const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]);
- const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-";
-
- return !permissions ? (null) : (
- <div
- key={userKey}
- className={"container"}
- >
- <span className={"padding"}>{user.email}</span>
- <div className="edit-actions">
- {admin || this.myDocAcls ? (
- <select
- className={"permissions-dropdown"}
- value={permissions}
- onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}
- >
- {this.sharingOptions(uniform)}
- </select>
- ) : (
- <div className={"permissions-dropdown"}>
- {permissions}
- </div>
+ const userListContents: (JSX.Element | null)[] = users
+ .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+ .map(({ user, linkDatabase, sharingDoc, userColor }) => {
+ const userKey = `acl-${normalizeEmail(user.email)}`;
+ const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]));
+ const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+
+ return !permissions ? null : (
+ <div key={userKey} className={'container'}>
+ <span className={'padding'}>{user.email}</span>
+ <div className="edit-actions">
+ {admin || this.myDocAcls ? (
+ <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}>
+ {this.sharingOptions(uniform)}
+ </select>
+ ) : (
+ <div className={'permissions-dropdown'}>{permissions}</div>
)}
+ </div>
</div>
- </div>
- );
- });
+ );
+ });
// checks if every doc has the same author
const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
// the owner of the doc and the current user are placed at the top of the user list.
userListContents.unshift(
- sameAuthor ?
- (
- <div
- key={"owner"}
- className={"container"}
- >
- <span className={"padding"}>{targetDoc?.author === Doc.CurrentUserEmail ? "Me" : targetDoc?.author}</span>
- <div className="edit-actions">
- <div className={"permissions-dropdown"}>
- Owner
- </div>
- </div>
+ sameAuthor ? (
+ <div key={'owner'} className={'container'}>
+ <span className={'padding'}>{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : targetDoc?.author}</span>
+ <div className="edit-actions">
+ <div className={'permissions-dropdown'}>Owner</div>
</div>
- ) : null,
- sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ?
- (
- <div
- key={"me"}
- className={"container"}
- >
- <span className={"padding"}>Me</span>
- <div className="edit-actions">
- <div className={"permissions-dropdown"}>
- {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : "-multiple-"}
- </div>
- </div>
+ </div>
+ ) : null,
+ sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? (
+ <div key={'me'} className={'container'}>
+ <span className={'padding'}>Me</span>
+ <div className="edit-actions">
+ <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div>
</div>
- ) : null
+ </div>
+ ) : null
);
-
// the list of groups shared with
- const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
- groupListMap.unshift({ title: "Public" });//, { title: "ALL" });
+ const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true));
+ groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
const groupKey = `acl-${StrCast(group.title)}`;
- const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]);
- const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-";
-
- return !permissions ? (null) : (
- <div
- key={groupKey}
- className={"container"}
- >
- <div className={"padding"}>{group.title}</div>
- {group instanceof Doc ?
- (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}>
- <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
- </div>)
- : (null)}
+ const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
+ const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
+
+ return !permissions ? null : (
+ <div key={groupKey} className={'container'}>
+ <div className={'padding'}>{StrCast(group.title)}</div>
+ {group instanceof Doc ? (
+ <div className="group-info" onClick={action(() => (GroupManager.Instance.currentGroup = group))}>
+ <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
+ </div>
+ ) : null}
<div className="edit-actions">
{admin || this.myDocAcls ? (
- <select
- className={"permissions-dropdown"}
- value={permissions}
- onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
- >
- {this.sharingOptions(uniform, group.title === "Override")}
+ <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
+ {this.sharingOptions(uniform, group.title === 'Override')}
</select>
) : (
- <div className={"permissions-dropdown"}>
- {permissions}
- </div>
- )}
+ <div className={'permissions-dropdown'}>{permissions}</div>
+ )}
</div>
</div>
);
});
return (
- <div className={"sharing-interface"}>
- {GroupManager.Instance?.currentGroup ?
- <GroupMemberView
- group={GroupManager.Instance.currentGroup}
- onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)}
- /> :
- null}
+ <div className={'sharing-interface'}>
+ {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null}
<div className="sharing-contents">
- <p className={"share-title"}><b>Share </b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, "this document") : "-multiple-")}</p>
- <div className={"close-button"} onClick={this.close}>
- <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
+ <p className={'share-title'}>
+ <b>Share </b>
+ {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
+ </p>
+ <div className={'close-button'} onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
</div>
{/* {this.linkVisible ?
<div>
{this.sharingUrl}
</div> :
(null)} */}
- {<div className="share-container">
- <div className="share-setup">
- <Select
- className="user-search"
- placeholder="Enter user or group name..."
- isMulti
- isSearchable
- closeMenuOnSelect={false}
- options={options}
- onChange={this.handleUsersChange}
- value={this.selectedUsers}
- styles={{
- indicatorSeparator: () => ({
- visibility: "hidden"
- })
- }}
- />
- <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}>
- {this.sharingOptions(true)}
- </select>
- <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
- Share
- </button>
- </div>
- <div className="sort-checkboxes">
- <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label>
- <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label>
- </div>
+ {
+ <div className="share-container">
+ <div className="share-setup">
+ <Select
+ className="user-search"
+ placeholder="Enter user or group name..."
+ isMulti
+ isSearchable
+ closeMenuOnSelect={false}
+ options={options}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={this.handleUsersChange}
+ value={this.selectedUsers}
+ styles={{
+ indicatorSeparator: () => ({
+ visibility: 'hidden',
+ }),
+ }}
+ />
+ <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}>
+ {this.sharingOptions(true)}
+ </select>
+ <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
+ Share
+ </button>
+ </div>
+ <div className="sort-checkboxes">
+ <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label>
+ <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label>
+ </div>
- <div className="acl-container">
- {Doc.UserDoc().noviceMode ? (null) :
- <div className="layoutDoc-acls">
- <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label>
- </div>}
+ <div className="acl-container">
+ {Doc.noviceMode ? null : (
+ <div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
+ </div>
+ )}
+ </div>
</div>
- </div>
}
<div className="main-container">
- <div className={"individual-container"}>
- <div
- className="user-sort"
- onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}>
- Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} />
- : this.individualSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} />
- : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />}
- </div>
- <div className={"users-list"}>
- {userListContents}
+ <div className={'individual-container'}>
+ <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}>
+ Individuals{' '}
+ {this.individualSort === 'ascending' ? (
+ <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
+ ) : this.individualSort === 'descending' ? (
+ <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
+ ) : (
+ <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
+ )}
</div>
+ <div className={'users-list'}>{userListContents}</div>
</div>
- <div className={"group-container"}>
- <div
- className="user-sort"
- onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
- Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} />
- : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} />
- : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />}
-
- </div>
- <div className={"groups-list"}>
- {groupListContents}
+ <div className={'group-container'}>
+ <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
+ Groups{' '}
+ {this.groupSort === 'ascending' ? (
+ <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
+ ) : this.groupSort === 'descending' ? (
+ <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
+ ) : (
+ <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
+ )}
</div>
+ <div className={'groups-list'}>{groupListContents}</div>
</div>
</div>
-
</div>
</div>
);
}
render() {
- return <MainViewModal
- contents={this.sharingInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
- overlayDisplayedOpacity={this.overlayOpacity}
- closeOnExternalClick={this.close}
- />;
+ return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 069f81d38..2c0a1da8b 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,8 +1,8 @@
-import { observable, action, runInAction } from "mobx";
-import { computedFn } from "mobx-utils";
+import { observable, action, runInAction } from 'mobx';
+import { computedFn } from 'mobx-utils';
+import { Doc } from '../../fields/Doc';
export namespace SnappingManager {
-
class Manager {
@observable IsDragging: boolean = false;
@observable public horizSnapLines: number[] = [];
@@ -15,25 +15,34 @@ export namespace SnappingManager {
this.horizSnapLines = horizLines;
this.vertSnapLines = vertLines;
}
-
- @observable cachedGroups: string[] = [];
- @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; }
}
const manager = new Manager();
- export function clearSnapLines() { manager.clearSnapLines(); }
- export function setSnapLines(horizLines: number[], vertLines: number[]) { manager.setSnapLines(horizLines, vertLines); }
- export function horizSnapLines() { return manager.horizSnapLines; }
- export function vertSnapLines() { return manager.vertSnapLines; }
+ export function clearSnapLines() {
+ manager.clearSnapLines();
+ }
+ export function setSnapLines(horizLines: number[], vertLines: number[]) {
+ manager.setSnapLines(horizLines, vertLines);
+ }
+ export function horizSnapLines() {
+ return manager.horizSnapLines;
+ }
+ export function vertSnapLines() {
+ return manager.vertSnapLines;
+ }
- export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
- export function GetIsDragging() { return manager.IsDragging; }
+ export function SetIsDragging(dragging: boolean) {
+ runInAction(() => (manager.IsDragging = dragging));
+ }
+ export function GetIsDragging() {
+ return manager.IsDragging;
+ }
- /// 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);
- export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); }
- export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); }
+ export function SetShowSnapLines(show: boolean) {
+ runInAction(() => (Doc.UserDoc().showSnapLines = show));
+ }
+ export function GetShowSnapLines() {
+ return Doc.UserDoc().showSnapLines;
+ }
}
-
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
new file mode 100644
index 000000000..4a2ccd706
--- /dev/null
+++ b/src/client/util/TrackMovements.ts
@@ -0,0 +1,270 @@
+import { IReactionDisposer, observable, observe, reaction } from 'mobx';
+import { NumCast } from '../../fields/Types';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { Id } from '../../fields/FieldSymbols';
+
+export type Movement = {
+ time: number;
+ panX: number;
+ panY: number;
+ scale: number;
+ docId: string;
+};
+
+export type Presentation = {
+ movements: Movement[] | null;
+ totalTime: number;
+ meta: Object | Object[];
+};
+
+export class TrackMovements {
+ private static get NULL_PRESENTATION(): Presentation {
+ return { movements: null, meta: {}, totalTime: -1 };
+ }
+
+ // instance variables
+ private currentPresentation: Presentation;
+ private tracking: boolean;
+ private absoluteStart: number;
+ // instance variable for holding the FFViews and their disposers
+ private recordingFFViews: Map<string, IReactionDisposer> | null;
+ private tabChangeDisposeFunc: IReactionDisposer | null;
+
+ // create static instance and getter for global use
+ @observable static _instance: TrackMovements;
+ static get Instance(): TrackMovements {
+ return TrackMovements._instance;
+ }
+ constructor() {
+ // init the global instance
+ TrackMovements._instance = this;
+
+ // init the instance variables
+ this.currentPresentation = TrackMovements.NULL_PRESENTATION;
+ this.tracking = false;
+ this.absoluteStart = -1;
+
+ // used for tracking movements in the view frame
+ this.recordingFFViews = null;
+ this.tabChangeDisposeFunc = null;
+ }
+
+ // little helper :)
+ private get nullPresentation(): boolean {
+ return this.currentPresentation.movements === null;
+ }
+
+ private addRecordingFFView(doc: Doc, key: string = doc[Id]): void {
+ // console.info('adding dispose func : docId', key, 'doc', doc);
+
+ if (this.recordingFFViews === null) {
+ console.warn('addFFView on null RecordingApi');
+ return;
+ }
+ if (this.recordingFFViews.has(key)) {
+ console.warn('addFFView : key already in map');
+ return;
+ }
+
+ const disposeFunc = reaction(
+ () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0) }),
+ res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, key, res.scale)
+ );
+ this.recordingFFViews?.set(key, disposeFunc);
+ }
+
+ private removeRecordingFFView = (key: string) => {
+ // console.info('removing dispose func : docId', key);
+ if (this.recordingFFViews === null) {
+ console.warn('removeFFView on null RecordingApi');
+ return;
+ }
+ this.recordingFFViews.get(key)?.();
+ this.recordingFFViews.delete(key);
+ };
+
+ // in the case where only one tab was changed (updates not across dashboards), set only one to true
+ private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => {
+ if (this.recordingFFViews === null) return;
+
+ // so that the size comparisons are correct, we must filter to only the FFViews
+ const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
+ const tabbedFFViews = new Set<string>();
+ for (const DashDoc of tabbedDocs) {
+ if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]);
+ }
+
+ // new tab was added - need to add it
+ if (tabbedFFViews.size > this.recordingFFViews.size) {
+ for (const DashDoc of tabbedDocs) {
+ if (!this.recordingFFViews.has(DashDoc[Id])) {
+ if (isFFView(DashDoc)) {
+ this.addRecordingFFView(DashDoc);
+
+ // only one max change, so return
+ if (onlyOne) return;
+ }
+ }
+ }
+ }
+ // tab was removed - need to remove it from recordingFFViews
+ else if (tabbedFFViews.size < this.recordingFFViews.size) {
+ for (const [key] of this.recordingFFViews) {
+ if (!tabbedFFViews.has(key)) {
+ this.removeRecordingFFView(key);
+ if (onlyOne) return;
+ }
+ }
+ }
+ };
+
+ private initTabTracker = () => {
+ if (this.recordingFFViews === null) {
+ this.recordingFFViews = new Map();
+ }
+
+ // init the dispose funcs on the page
+ const docList = DocListCast(CollectionDockingView.Instance?.props.Document.data);
+ this.updateRecordingFFViewsFromTabs(docList);
+
+ // create a reaction to monitor changes in tabs
+ this.tabChangeDisposeFunc = reaction(
+ () => CollectionDockingView.Instance?.props.Document.data,
+ change => {
+ // TODO: consider changing between dashboards
+ // console.info('change in tabs', change);
+ this.updateRecordingFFViewsFromTabs(DocListCast(change), true);
+ }
+ );
+ };
+
+ start = (meta?: Object) => {
+ this.initTabTracker();
+
+ // 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 tracking true to allow trackMovements
+ this.tracking = true;
+ };
+
+ /* stops the video and returns the presentatation; if no presentation, returns undefined */
+ yieldPresentation(clearData: boolean = true): Presentation | null {
+ // if no presentation or done tracking, return null
+ if (this.nullPresentation || !this.tracking) return null;
+
+ // set the previus recording view to the play view
+ // this.playFFView = this.recordingFFView;
+
+ // ensure we add the endTime now that they are done recording
+ const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+
+ // reset the current presentation
+ clearData && this.clear();
+
+ // console.info('yieldPresentation', cpy);
+ return cpy;
+ }
+
+ finish = (): void => {
+ // make is tracking false
+ this.tracking = false;
+ // reset the RecordingApi instance
+ this.clear();
+ };
+
+ private clear = (): void => {
+ // clear the disposeFunc if we are done (not tracking)
+ if (!this.tracking) {
+ this.removeAllRecordingFFViews();
+ this.tabChangeDisposeFunc?.();
+ // update the presentation mode now that we are done tracking
+ Doc.UserDoc().presentationMode = 'none';
+
+ this.recordingFFViews = null;
+ this.tabChangeDisposeFunc = null;
+ }
+
+ // clear presenation data
+ this.currentPresentation = TrackMovements.NULL_PRESENTATION;
+ // clear absoluteStart
+ this.absoluteStart = -1;
+ };
+
+ removeAllRecordingFFViews = () => {
+ if (this.recordingFFViews === null) {
+ console.warn('removeAllFFViews on null RecordingApi');
+ return;
+ }
+
+ for (const [id, disposeFunc] of this.recordingFFViews) {
+ // console.info('calling dispose func : docId', id);
+ disposeFunc();
+ this.recordingFFViews.delete(id);
+ }
+ };
+
+ private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => {
+ // ensure we are recording to track
+ if (!this.tracking) {
+ console.error('[recordingApi.ts] trackMovements(): tracking is false');
+ return;
+ }
+ // check to see if the presetation is init - if not, we are between segments
+ // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres)
+ // bacuse tracking should be false inbetween segments high key
+ if (this.nullPresentation) {
+ console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments');
+ return;
+ }
+
+ // get the time
+ const time = new Date().getTime() - this.absoluteStart;
+ // make new movement object
+ const movement: Movement = { time, panX, panY, scale, docId };
+
+ // add that movement to the current presentation data's movement array
+ this.currentPresentation.movements && this.currentPresentation.movements.push(movement);
+ };
+
+ // method that concatenates an array of presentatations into one
+ public concatPresentations = (presentations: Presentation[]): Presentation => {
+ // these three will lead to the combined presentation
+ let combinedMovements: Movement[] = [];
+ let sumTime = 0;
+ let combinedMetas: any[] = [];
+
+ presentations.forEach(presentation => {
+ const { movements, totalTime, meta } = presentation;
+
+ // update movements if they had one
+ if (movements) {
+ // add the summed time to the movements
+ const addedTimeMovements = movements.map(move => {
+ return { ...move, time: move.time + sumTime };
+ });
+ // concat the movements already in the combined presentation with these new ones
+ combinedMovements.push(...addedTimeMovements);
+ }
+
+ // update the totalTime
+ sumTime += totalTime;
+
+ // concatenate the metas
+ combinedMetas.push(meta);
+ });
+
+ // return the combined presentation with the updated total summed time
+ return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
+ };
+}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d1f1a0099..d0aec45a6 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,5 +1,5 @@
-import { observable, action, runInAction } from "mobx";
-import { Without } from "../../Utils";
+import { observable, action, runInAction } from 'mobx';
+import { Without } from '../../Utils';
function getBatchName(target: any, key: string | symbol): string {
const keyName = key.toString();
@@ -28,9 +28,9 @@ function propertyDecorator(target: any, key: string | symbol) {
} finally {
batch.end();
}
- }
+ },
});
- }
+ },
});
}
@@ -39,7 +39,7 @@ export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
if (!key) {
return function () {
- const batch = UndoManager.StartBatch("");
+ const batch = UndoManager.StartBatch('');
try {
return target.apply(undefined, arguments);
} finally {
@@ -96,7 +96,7 @@ export namespace UndoManager {
}
export function PrintBatches(): void {
- console.log("Open Undo Batches:");
+ console.log('Open Undo Batches:');
GetOpenBatches().forEach(batch => console.log(batch.batchName));
}
@@ -106,23 +106,25 @@ export namespace UndoManager {
}
export function FilterBatches(fieldTypes: string[]) {
const fieldCounts: { [key: string]: number } = {};
- const lastStack = UndoManager.undoStack.slice(-1)[0];//.lastElement();
+ const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement();
if (lastStack) {
lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
const fieldCount2: { [key: string]: number } = {};
- runInAction(() =>
- UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
- if (fieldTypes.includes(ev.prop)) {
- fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
- if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
- return false;
- }
- return true;
- }));
+ runInAction(
+ () =>
+ (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
+ if (fieldTypes.includes(ev.prop)) {
+ fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
+ if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
+ return false;
+ }
+ return true;
+ }))
+ );
}
}
export function TraceOpenBatches() {
- console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`);
+ console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join('\n\t')}\n`);
}
export class Batch {
private disposed: boolean = false;
@@ -133,15 +135,15 @@ export namespace UndoManager {
private dispose = (cancel: boolean) => {
if (this.disposed) {
- throw new Error("Cannot dispose an already disposed batch");
+ throw new Error('Cannot dispose an already disposed batch');
}
this.disposed = true;
openBatches.splice(openBatches.indexOf(this));
- EndBatch(cancel);
- }
+ return EndBatch(cancel);
+ };
- end = () => { this.dispose(false); };
- cancel = () => { this.dispose(true); };
+ end = () => this.dispose(false);
+ cancel = () => this.dispose(true);
}
export function StartBatch(batchName: string): Batch {
@@ -163,7 +165,9 @@ export namespace UndoManager {
}
redoStack.length = 0;
currentBatch = undefined;
+ return true;
}
+ return false;
});
export function RunInTempBatch<T>(fn: () => T) {
@@ -232,5 +236,4 @@ export namespace UndoManager {
undoStack.push(commands);
});
-
-} \ No newline at end of file
+}