aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CaptureManager.tsx39
-rw-r--r--src/client/util/CurrentUserUtils.ts479
-rw-r--r--src/client/util/DictationManager.ts22
-rw-r--r--src/client/util/DocumentManager.ts479
-rw-r--r--src/client/util/DragManager.ts144
-rw-r--r--src/client/util/DropConverter.ts22
-rw-r--r--src/client/util/GroupManager.tsx8
-rw-r--r--src/client/util/HypothesisUtils.ts2
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx342
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts32
-rw-r--r--src/client/util/InteractionUtils.tsx10
-rw-r--r--src/client/util/LinkFollower.ts190
-rw-r--r--src/client/util/LinkManager.ts204
-rw-r--r--src/client/util/RTFMarkup.tsx137
-rw-r--r--src/client/util/ReplayMovements.ts167
-rw-r--r--src/client/util/ReportManager.tsx2
-rw-r--r--src/client/util/Scripting.ts40
-rw-r--r--src/client/util/SearchUtil.ts94
-rw-r--r--src/client/util/SelectionManager.ts64
-rw-r--r--src/client/util/SerializationHelper.ts97
-rw-r--r--src/client/util/ServerStats.tsx54
-rw-r--r--src/client/util/SettingsManager.scss16
-rw-r--r--src/client/util/SettingsManager.tsx46
-rw-r--r--src/client/util/SharingManager.tsx114
-rw-r--r--src/client/util/SnappingManager.ts4
-rw-r--r--src/client/util/TrackMovements.ts43
-rw-r--r--src/client/util/UndoManager.ts73
-rw-r--r--src/client/util/request-image-size.ts (renamed from src/client/util/request-image-size.js)22
28 files changed, 1593 insertions, 1353 deletions
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 0b5957fac..d68761ba7 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -1,19 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable } 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 { Doc } from '../../fields/Doc';
+import { DocCast, StrCast } from '../../fields/Types';
+import { addStyleSheet } from '../../Utils';
import { LightboxView } from '../views/LightboxView';
import { MainViewModal } from '../views/MainViewModal';
import './CaptureManager.scss';
+import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
-import { undoBatch } from './UndoManager';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
@observer
export class CaptureManager extends React.Component<{}> {
@@ -53,23 +49,14 @@ export class CaptureManager extends React.Component<{}> {
const doc = this._document;
const order: JSX.Element[] = [];
if (doc) {
- console.log('title', doc.title);
- console.log('links', doc.links);
- const linkDocs = DocListCast(doc.links);
- const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1
- const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2
- linkDocs.forEach((l, i) => {
- if (l) {
- console.log(i, (l.anchor1 as Doc).title);
- console.log(i, (l.anchor2 as Doc).title);
- order.push(
- <div className="list-item">
- <div className="number">{i}</div>
- {StrCast((l.anchor1 as Doc).title)}
- </div>
- );
- }
- });
+ LinkManager.Links(doc).forEach((l, i) =>
+ order.push(
+ <div className="list-item">
+ <div className="number">{i}</div>
+ {StrCast(DocCast(l.link_anchor_1)?.title)}
+ </div>
+ )
+ );
}
return (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 114dbac93..11a8dcaf6 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,15 +1,14 @@
-import { forOwn } from "lodash";
import { reaction } from "mobx";
import * as rp from 'request-promise';
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { Id } from "../../fields/FieldSymbols";
+import { FieldLoader } from "../../fields/FieldLoader";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
-import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
+import { ScriptField } from "../../fields/ScriptField";
+import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
import { SetCachedGroups, SharingPermissions } from "../../fields/util";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
@@ -21,10 +20,12 @@ 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 { ButtonType } from "../views/nodes/button/FontIconBox";
+import { OpenWhere } from "../views/nodes/DocumentView";
import { OverlayView } from "../views/OverlayView";
-import { DragManager } from "./DragManager";
+import { DragManager, dropActionType } from "./DragManager";
import { MakeTemplate } from "./DropConverter";
+import { FollowLinkScript } from "./LinkFollower";
import { LinkManager } from "./LinkManager";
import { ScriptingGlobals } from "./ScriptingGlobals";
import { ColorScheme } from "./SettingsManager";
@@ -36,14 +37,18 @@ interface Button {
toolTip?: string;
icon?: string;
btnType?: ButtonType;
- numBtnType?: NumButtonType;
numBtnMin?: number;
numBtnMax?: number;
switchToggle?: boolean;
width?: number;
+ linearBtnWidth?: number;
+ toolType?: string; // type of pen tool
+ expertMode?: boolean;// available only in expert mode
btnList?: List<string>;
ignoreClick?: boolean;
buttonText?: string;
+ backgroundColor?: string;
+ waitForDoubleClickToClick?: boolean;
// fields that do not correspond to DocumentOption fields
scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
@@ -59,11 +64,11 @@ export class CurrentUserUtils {
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 },
+ templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, isSystem: 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) })
+ Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, isSystem: true }),
+ Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
], opts)
},
{
@@ -79,7 +84,7 @@ export class CurrentUserUtils {
];
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 reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' };
const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
if (templateBtn) {
DocUtils.AssignOpts(templateBtn,btnOpts);
@@ -91,9 +96,9 @@ export class CurrentUserUtils {
});
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,
+ title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true,
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true,
+ _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
const reqdFuncs = { hidden: "IsNoviceMode()" };
@@ -103,55 +108,55 @@ export class CurrentUserUtils {
/// 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 reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: 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)"}];
+ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(documentView?.props.docViewPath().lastElement()?.rootDoc.target).then((target) => target && (target.proto.data = new List([self])))"},
+ { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}];
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),allOpts);
});
- const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true};
+ const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true};
return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
}
/// Initializes templates for editing click funcs of a document
- static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") {
+ static setupClickEditorTemplates(doc: Doc, field = "template_clickFuncs") {
const tempClicks = DocCast(doc[field]);
- const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true};
+ const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, isSystem: 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)"},
+ { opts: { title: "onClick"}, script: "console.log('click')"},
+ { opts: { title: "onDoubleClick"}, script: "console.log('click')"},
+ { opts: { title: "onChildClick"}, script: "console.log('click')"},
+ { opts: { title: "onChildDoubleClick"}, script: "console.log('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 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?.toString());
});
- const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true};
+ const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: 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") {
+ 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, autoHeight: true, fitWidth: true, system: true};
+ const reqdOpts = {...opts, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: 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");
});
- const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true };
+ const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
}
@@ -163,14 +168,14 @@ export class CurrentUserUtils {
CurrentUserUtils.setupClickEditorTemplates(doc)
];
CurrentUserUtils.setupChildClickEditors(doc)
- const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, };
+ const reqdOpts = { title: "template layouts", _xMargin: 0, isSystem: 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 };
+ static setupDefaultIconTemplates(doc: Doc, field="template_icons") {
+ const reqdOpts = { title: "icon templates", _height: 75, isSystem: true };
const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => {
@@ -181,22 +186,22 @@ export class CurrentUserUtils {
case DocumentType.IMG : creator = imageBox; break;
case DocumentType.FONTICON: creator = fontBox; break;
}
- const allopts = {system: true, ...opts};
+ const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts};
return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ?
DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
- {onClick:"deiconifyView(documentView)"});
+ {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(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
+ textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 24, layout_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 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, _layout_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.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}),
makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
@@ -212,9 +217,9 @@ export class CurrentUserUtils {
/// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyTrail
static creatorBtnDescriptors(doc: Doc): {
title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc,
- backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
+ backgroundColor?: string, openFactoryAsDelegate?:boolean, openFactoryLocation?:string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
}[] {
- const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
+ const standardOps = (key:string) => ({ title : "Untitled "+ key, _layout_fitWidth: false, isSystem: true, "dragFactory_count": 0, cloneFieldFilter: new List<string>(["isSystem"]) });
const json = {
doc: {
type: "doc",
@@ -222,11 +227,11 @@ export class CurrentUserUtils {
{
type: "paragraph", attrs: {}, content: [{
type: "dashField",
- attrs: { fieldKey: "author", docid: "", hideKey: false },
+ attrs: { fieldKey: "author", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}, {
type: "dashField",
- attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+ attrs: { fieldKey: "author_date", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}]
}]
@@ -241,7 +246,7 @@ export class CurrentUserUtils {
"<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' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='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 fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._layout_autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
"</HTMLdiv>"
}, "header");
@@ -255,69 +260,78 @@ export class CurrentUserUtils {
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
+ scripts?:{[key:string]: any},
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: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }},
+ {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }},
+ {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
+ {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }},
+ {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
+ {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_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: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, _layout_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: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }},
+ {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{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: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _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
+ {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
+ {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }},
+ {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
+ {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
+ treeViewHasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ dropAction:'move', treeViewType: TreeViewType.outline,
+ backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: 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));
+ emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, 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 bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, funcs: { hidden: 'IsNoviceMode()'}},
- { 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, }))
+ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)},
+ { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)},
+ { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)},
+ { toolTip: "Tap or drag to create a physics simulation", title: "Simulation", icon: "atom", dragFactory: doc.emptySimulation as Doc, },
+ { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
+ { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
+ { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)},
+ { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
+ { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)},
+ { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
+ { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)},
+ { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
+ { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true },
+ { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
+ { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack>" as any, openFactoryLocation: OpenWhere.overlay},
+ ].map(tuple => (
+ { openFactoryLocation: OpenWhere.addRight,
+ scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)',
+ onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }'},
+ ...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"]),
+ const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
+ _width: 35, _height: 35, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true,
+ btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs);
});
const reqdOpts:DocumentOptions = {
- title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
- _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- childDocumentsActive: true
+ title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
+ _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
+ childDragAction: 'embed'
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -342,7 +356,7 @@ export class CurrentUserUtils {
/// 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});
+ DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as any), {isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
}
/// Initializes the left sidebar menu buttons and the panels they open up
@@ -352,23 +366,22 @@ export class CurrentUserUtils {
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"]),
+ title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true,
+ _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
});
const reqdStackOpts:DocumentOptions ={
- title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
- _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true
+ title: "menuItemPanel", childDragAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
+ _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
}
// Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu
static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") {
- const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,};
+ const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, isSystem: true, _chromeHidden: true,};
DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts);
}
@@ -389,62 +402,62 @@ export class CurrentUserUtils {
title: data.title,
_lockedPosition: true,
onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
- backgroundColor: data.backgroundColor, system: true
+ backgroundColor: data.backgroundColor, isSystem: true
},
- [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
+ [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", isSystem: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
);
}
// sets up the main document for the mobile button
static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
...opts,
- _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
- borderRounding: "5px", boxShadow: "0 0", system: true
+ _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
+ layout_borderRounding: "5px", layout_boxShadow: "0 0", isSystem: true
}) as any as Doc
// sets up the text container for the information contained within the mobile button
static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
...opts,
- _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
- backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true
+ _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
+ backgroundColor: "rgba(0,0,0,0)", layout_borderRounding: "0", layout_boxShadow: "0 0", ignoreClick: true, isSystem: true
}) as any as Doc
// Sets up the title of the button
static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, {
...opts,
- title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", system: true
+ title: buttonTitle, _text_fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true
}) as any as Doc
// Sets up the description of the button
static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, {
...opts,
- title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true
+ title: "info", _text_fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true
}) as any as Doc
static setupMobileInkingDoc(userDoc: Doc) {
- return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", system: true });
+ return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", isSystem: true });
}
static setupMobileUploadDoc(userDoc: Doc) {
// const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
- title: "Upload Images From the Web", _lockedPosition: true, system: true
+ title: "Upload Images From the Web", _lockedPosition: true, isSystem: true
});
const uploadDoc = Docs.Create.StackingDocument([], {
- title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true, _chromeHidden: true,
+ title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, isSystem: true, _chromeHidden: true,
});
return Docs.Create.StackingDocument([webDoc, uploadDoc], {
- _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true, _chromeHidden: true,
+ _width: screen.width, _lockedPosition: true, title: "Upload", _layout_autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", isSystem: true, _chromeHidden: true,
});
}
/// 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, });
+ dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: "embed",
+ _lockedPosition: true, _type_collection: CollectionViewType.Schema });
}
/// Initializes the panel of draggable tools that is opened from the left sidebar.
@@ -453,8 +466,8 @@ export class CurrentUserUtils {
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,
+ title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
+ _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]);
}
@@ -466,10 +479,10 @@ export class CurrentUserUtils {
const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`;
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 reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
+ title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true };
const reqdBtnScript = {onClick: newDashboard,}
- const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
+ const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
const contextMenuScripts = [/*newDashboard*/] as string[];
const contextMenuLabels = [/*"Create New Dashboard"*/] as string[];
@@ -479,15 +492,15 @@ export class CurrentUserUtils {
const childContextMenuLabels = ["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
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: 350, ignoreClick: true,
- buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias",
- _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
+ title: "My Dashboards", childHideLinkButton: true, treeViewFreezeChildren: "remove|add", treeViewHideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ dropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true,
+ layout_headerButton: newDashboardButton, childDragAction: "embed",
+ _layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
contextMenuLabels:new List<string>(contextMenuLabels),
contextMenuIcons:new List<string>(contextMenuIcons),
childContextMenuLabels:new List<string>(childContextMenuLabels),
childContextMenuIcons:new List<string>(childContextMenuIcons),
- 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."
+ layout_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);
if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
@@ -505,23 +518,23 @@ export class CurrentUserUtils {
/// 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 myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", undoIgnoreFields:new List<string>(['treeViewSortCriterion']), _dragOnlyWithinContainer: true, isSystem: 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
+ _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
};
const newFolderScript = { onClick: newFolder};
- const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
+ const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), 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: 350, ignoreClick: true, childDropAction: "alias",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
+ title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, dropAction: "proto", isSystem: true,
+ isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
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."
+ layout_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];
@@ -533,22 +546,22 @@ export class CurrentUserUtils {
/// 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: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
+ title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDragAction: "embed", isSystem: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "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."
+ layout_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);
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,
+ const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
+ title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true,
toolTip: "Empty recently closed",};
- const clearDocsButton = DocUtils.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
+ DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
- if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton;
+ //if (recentlyClosed.layout_headerButton !== clearDocsButton) Doc.GetProto(recentlyClosed).layout_headerButton = clearDocsButton;
if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) {
recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!])
@@ -560,7 +573,7 @@ export class CurrentUserUtils {
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,
+ layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true,
treeViewHideTitle: true, treeViewTruncateTitleWidth: 350
};
if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
@@ -568,15 +581,15 @@ export class CurrentUserUtils {
}
static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, {
- ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true,
+ ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
- _lockedPosition: true, system: true, flexDirection: "row"
+ _lockedPosition: true, isSystem: true, flexDirection: "row"
})
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,
+ btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true,
+ _dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]),
+ _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, isSystem: true, ...opts,
})
/// initializes the required buttons in the expanding button menu at the bottom of the Dash window
@@ -587,72 +600,87 @@ export class CurrentUserUtils {
CurrentUserUtils.createToolButton(opts), scripts, funcs);
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" }},
- { scripts: { }, opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
- { scripts: { }, opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing audio"}},
- // { scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, opts: { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, width: 20}},
- // { scripts: { onClick:""}, opts: { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, width: 20}, funcs: { buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}},
- // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, width: 20,} },
- ];
- 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, boxShadow: "standard",
- childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
+ { 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" }},
+ { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet)
+ { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}},
+ { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}},
+ ];
+ const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts));
+ const dockBtnsReqdOpts:DocumentOptions = {
+ title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+ childDontRegisterViews: true, linearView_IsExpanded: true, linearView_Expandable: 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 freeTools(): Button[] {
+ return [
+ { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+ { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+ { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag (double click to set for all)",waitForDoubleClickToClick:true, btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(false, _readOnly_)', onDoubleClick:`{ return toggleRaiseOnDrag(true, _readOnly_)`}}, // Only when floating document is selected in freeform
+ ]
+ }
+ static viewTools(): Button[] {
+ return [
+ { title: "Snap\xA0Lines", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "View\xA0All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ ]
+ }
static textTools():Button[] {
return [
- { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'},
+ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, 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: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, width: 75, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
+ { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
+ { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+ { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
+ { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
// { 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_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
- { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
- ];
+ ];
}
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
- // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
- { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
- { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
- { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
- { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", scripts: {onClick:'{ return setIsInkMask(_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_);}'} },
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}'} },
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+ { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnMin: 1},
+ { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
];
}
static schemaTools():Button[] {
- return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }];
+ return [
+ {title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} },
+ {title: "Single Lines", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, buttonText: "Single Line", icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
+ ];
}
static webTools() {
return [
{ title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }},
{ title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}},
- //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' },
{ title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
];
}
@@ -664,18 +692,20 @@ export class CurrentUserUtils {
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: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
- { 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: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: {}},
- { 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', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`}, scripts: { onClick: 'setInkToolDefaults()'} }, // 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
+ title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+ { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 20, scripts: { onClick: 'pinWithView(altKey)'}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, 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, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+ { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
+ { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
];
}
@@ -683,12 +713,12 @@ export class CurrentUserUtils {
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,
+ backgroundColor: params.backgroundColor ??"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, isSystem: 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"]),
+ _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
+ toolType: params.toolType, expertMode: params.expertMode,
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _lockedPosition: true,
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
@@ -699,16 +729,16 @@ export class CurrentUserUtils {
/// 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 reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsExpanded"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: 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,
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsExpanded"]),
childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearViewSubMenu: true, linearViewExpandable: true, };
+ linearView_SubMenu: true, linearView_Expandable: true, };
const items = params.subMenu?.map(sub =>
this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
);
@@ -721,11 +751,11 @@ export class CurrentUserUtils {
/// 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 });
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", isSystem: true });
}
static setupPublished(doc:Doc, field = "myPublishedDocs") {
- return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true });
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true });
}
/// The database of all links on all documents
@@ -746,22 +776,23 @@ export class CurrentUserUtils {
// 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 dashboardFilter = ScriptField.MakeFunction(`doc._type_collection === '${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)",
+ isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
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: 0, _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'"
+ childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
+ // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
+ _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ layout_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);
@@ -770,17 +801,17 @@ export class CurrentUserUtils {
/// 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."
+ title: "My Imports", _forceActive: true, ignoreClick: true, _layout_showTitle: "title",
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0,
+ childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
+ dontRegisterView: true, layout_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()" });
+ _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
+ buttonText: "Import", icon: "upload", isSystem: true };
+ DocUtils.AssignDocField(myImports, "layout_headerButton", (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
@@ -788,20 +819,22 @@ export class CurrentUserUtils {
/// 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"]),
+ reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data_modificationDate"]),
async () => {
const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
}, { fireImmediately: true });
- doc.system ?? (doc.system = true);
+ doc.isSystem ?? (doc.isSystem = 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.activeTool = InkTool.None;
- doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
+ doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
+ doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
+ doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
doc.activeInkWidth ?? (doc.activeInkWidth = 1);
doc.activeInkBezier ?? (doc.activeInkBezier = "0");
doc.activeFillColor ?? (doc.activeFillColor = "");
@@ -815,7 +848,7 @@ export class CurrentUserUtils {
doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false);
doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
doc.filterDocCount = 0;
- doc.freezeChildren = "remove|add";
+ doc.treeViewFreezeChildren = "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
@@ -828,8 +861,10 @@ export class CurrentUserUtils {
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
+ DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
+
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;
@@ -837,16 +872,17 @@ export class CurrentUserUtils {
new LinkManager();
setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
+ setInterval(DocServer.UPDATE_SERVER_CACHE, 120000);
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 fieldInfoOpts = { title: "Field Infos", isSystem: 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("_")};
+ const opts:DocumentOptions = { isSystem: 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;
@@ -868,10 +904,12 @@ export class CurrentUserUtils {
if (result.cacheDocumentIds)
{
const ids = result.cacheDocumentIds.split(";");
- const batch = 10000;
+ const batch = 30000;
+ FieldLoader.active = true;
for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) {
await DocServer.GetRefFields(ids.slice(i, i+batch));
}
+ FieldLoader.active = false;
}
return result;
} else {
@@ -945,6 +983,5 @@ ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Do
ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "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 setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); \ No newline at end of file
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 0a61f3478..717473aa1 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -5,13 +5,13 @@ import { Doc, Opt } from '../../fields/Doc';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
import { listSpec } from '../../fields/Schema';
-import { Cast, CastCtor } from '../../fields/Types';
+import { Cast, CastCtor, DocCast } from '../../fields/Types';
import { AudioField, ImageField } from '../../fields/URLField';
import { Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
@@ -324,28 +324,18 @@ export namespace DictationManager {
],
[
- 'open fields',
- {
- action: (target: DocumentView) => {
- const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
- target.props.addDocTab(kvp, 'add:right');
- },
- },
- ],
-
- [
'new outline',
{
action: (target: DocumentView) => {
- const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true });
- const proto = newBox.proto!;
+ const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _layout_autoHeight: true });
+ const proto = DocCast(newBox.proto);
const prompt = 'Press alt + r to start dictating here...';
const head = 3;
const anchor = head + prompt.length;
const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
proto.data = new RichTextField(proseMirrorState);
proto.backgroundColor = '#eeffff';
- target.props.addDocTab(newBox, 'add:right');
+ target.props.addDocTab(newBox, OpenWhere.addRight);
},
},
],
@@ -382,7 +372,7 @@ export namespace DictationManager {
expression: /view as (freeform|stacking|masonry|schema|tree)/g,
action: (target: DocumentView, matches: RegExpExecArray) => {
const mode = matches[1];
- mode && (target.props.Document._viewType = mode);
+ mode && (target.props.Document._type_collection = mode);
},
restrictTo: [DocumentType.COL],
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index b046d950f..7e3302067 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,27 +1,32 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { action, computed, observable, ObservableSet } from 'mobx';
+import { Doc, Opt } from '../../fields/Doc';
+import { Animation } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
-import { DocumentType } from '../documents/DocumentTypes';
+import { listSpec } from '../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
+import { CollectionViewType } from '../documents/DocumentTypes';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { TabDocView } from '../views/collections/TabDocView';
import { LightboxView } from '../views/LightboxView';
-import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from '../views/nodes/KeyValueBox';
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 { PresBox } from '../views/nodes/trails';
import { ScriptingGlobals } from './ScriptingGlobals';
import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
const { Howl } = require('howler');
export class DocumentManager {
//global holds all of the nodes (regardless of which collection they're in)
- @observable public DocumentViews = new Set<DocumentView>();
+ @observable _documentViews = new Set<DocumentView>();
@observable public LinkAnchorBoxViews: DocumentView[] = [];
@observable public RecordingEvent = 0;
@observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = [];
+ @computed public get DocumentViews() {
+ return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox));
+ }
private static _instance: DocumentManager;
public static get Instance(): DocumentManager {
@@ -31,26 +36,56 @@ export class DocumentManager {
//private constructor so no other class can create a nodemanager
private constructor() {}
+ private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+ public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => {
+ if (doc) {
+ const dv = this.getDocumentView(doc);
+ this._viewRenderedCbs.push({ doc, func });
+ if (dv) {
+ this.callAddViewFuncs(dv);
+ return true;
+ }
+ } else {
+ func(undefined as any);
+ }
+ return false;
+ };
+ callAddViewFuncs = (view: DocumentView) => {
+ const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
+ if (callFuncs.length) {
+ this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc));
+ const intTimer = setInterval(
+ () => {
+ if (!view.ComponentView?.incrementalRendering?.()) {
+ callFuncs.forEach(cf => cf.func(view));
+ clearInterval(intTimer);
+ }
+ },
+ view.ComponentView?.incrementalRendering?.() ? 0 : 100
+ );
+ }
+ };
+
@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';
- 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,
- })
- );
- });
+ const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1';
+ const link = view.rootDoc;
+ this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+ this.LinkedDocumentViews.push({
+ a: viewAnchorIndex === 'link_anchor_2' ? otherView : view,
+ b: viewAnchorIndex === 'link_anchor_2' ? 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.add(view);
+ this._documentViews.add(view);
}
+ this.callAddViewFuncs(view);
};
public RemoveView = action((view: DocumentView) => {
this.LinkedDocumentViews.slice().forEach(
@@ -66,7 +101,7 @@ export class DocumentManager {
const index = this.LinkAnchorBoxViews.indexOf(view);
this.LinkAnchorBoxViews.splice(index, 1);
} else {
- this.DocumentViews.delete(view);
+ this._documentViews.delete(view);
}
SelectionManager.DeselectView(view);
});
@@ -74,15 +109,14 @@ export class DocumentManager {
//gets all views
public getDocumentViewsById(id: string) {
const toReturn: DocumentView[] = [];
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
+ DocumentManager.Instance.DocumentViews.forEach(view => {
if (view.rootDoc[Id] === id) {
toReturn.push(view);
}
});
if (toReturn.length === 0) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- const doc = view.rootDoc.proto;
- if (doc && doc[Id] && doc[Id] === id) {
+ DocumentManager.Instance.DocumentViews.forEach(view => {
+ if (Doc.GetProto(view.rootDoc)?.[Id] === id) {
toReturn.push(view);
}
});
@@ -94,54 +128,52 @@ export class DocumentManager {
return this.getDocumentViewsById(doc[Id]);
}
- public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
- if (!id) return undefined;
- let toReturn: DocumentView | undefined;
- const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
-
- for (const pass of passes) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- if (view.rootDoc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
- toReturn = view;
- return;
- }
- });
- if (!toReturn) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- const doc = view.rootDoc.proto;
- if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
- toReturn = view;
- }
- });
- } else {
- break;
- }
- }
-
- return toReturn;
- }
-
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
- return this.getDocumentViewById(toFind[Id], preferredCollection);
+ public getDocumentView(toFind: Doc | undefined, preferredCollection?: DocumentView): DocumentView | undefined {
+ const doc =
+ // bcz: this was temporary code used to match documents by data url instead of by id. intended only for repairing the DB
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFind;
+ const docViewArray = DocumentManager.Instance.DocumentViews;
+ const passes = !doc ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined];
+ return passes.reduce(
+ (pass, toReturn) =>
+ toReturn ??
+ docViewArray.filter(view => view.rootDoc === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ??
+ docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, doc)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection),
+ undefined as Opt<DocumentView>
+ );
}
public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
const views: DocumentView[] = [];
- 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);
+ DocumentManager.Instance.DocumentViews.forEach(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);
+ if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc);
+ 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[] {
+ public getDocumentViews(toFindIn: Doc): DocumentView[] {
+ const toFind =
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFindIn;
+
const toReturn: DocumentView[] = [];
- 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));
+ const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
+ const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath));
// heuristic to return the "best" documents first:
// choose a document in the lightbox first
- // choose an exact match over an alias match
+ // choose an exact match over an embedding match
lightViews.map(view => view.rootDoc === toFind && toReturn.push(view));
lightViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view));
docViews.map(view => view.rootDoc === toFind && toReturn.push(view));
@@ -150,192 +182,159 @@ export class DocumentManager {
return toReturn;
}
+ static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
+ if (!doc) return [];
+ const srcContext = DocCast(doc.annotationOn, DocCast(doc.embedContainer));
+ var containerDocContext = srcContext ? [srcContext, doc] : [doc];
+ while (
+ containerDocContext.length &&
+ containerDocContext[0]?.embedContainer &&
+ DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking &&
+ (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0]))
+ ) {
+ containerDocContext = [Cast(containerDocContext[0].embedContainer, Doc, null), ...containerDocContext];
+ }
+ return containerDocContext;
+ }
+
+ static playAudioAnno(doc: Doc) {
+ const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
+ if (anno) {
+ if (anno instanceof AudioField) {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ });
+ }
+ }
+ }
+
+ public static removeOverlayViews() {
+ DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined)));
+ DocumentManager._overlayViews?.clear();
+ }
+ static _overlayViews = new ObservableSet<DocumentView>();
static addView = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, 'right');
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
finished?.();
};
- public jumpToDocument = (
+
+ // shows a documentView by:
+ // traverses down through the viewPath of contexts to the view:
+ // focusing on each context
+ public showDocumentView = async (targetDocView: DocumentView, options: DocFocusOptions) => {
+ const docViewPath = targetDocView.docViewPath.slice();
+ let rootContextView = docViewPath.shift();
+ await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined })));
+ if (options.toggleTarget && (!options.didMove || targetDocView.rootDoc.hidden)) targetDocView.rootDoc.hidden = !targetDocView.rootDoc.hidden;
+ else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.rootDoc, options.openLocation);
+ };
+
+ // shows a document by first:
+ // traversing down through the contexts that contain target until an existing view is found
+ // if no container view is found, create one by: opening an existing tab that has the top-level view, or showing the top-level context in the lightbox.
+ // once a containing view is found, it then traverses back down through the contexts to the target document by:
+ // focusing on each context
+ // and finally restoring the targetDoc to the viewSpec specified by the last document which may either be the targetDoc, or a viewSpec that describes the targetDoc configuration
+ public showDocument = async (
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
- 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,
- originalTarget?: Doc,
- noSelect?: boolean,
- presZoomScale?: number
- ): void => {
- originalTarget = originalTarget ?? targetDoc;
- const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
- const docView = getFirstDocView(targetDoc, originatingDoc);
- const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
- const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? 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
- finalTargetDoc.hidden = !finalTargetDoc.hidden;
- }
- } else {
- finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
- !noSelect && docView?.select(false);
- if (originatingDoc?.followLinkAudio) {
- const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement();
- if (anno) {
- if (anno instanceof AudioField) {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- });
- }
- }
+ options: DocFocusOptions, // options for how to navigate to target
+ finished?: () => void
+ ) => {
+ const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
+ if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
+ let rootContextView =
+ docContextPath.length &&
+ (await new Promise<DocumentView>(res => {
+ const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
+ if (viewIndex !== -1) {
+ viewIndex && docContextPath.splice(0, viewIndex);
+ return res(this.getDocumentView(docContextPath[0])!);
}
+ options.didMove = true;
+ docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+ this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
+ }));
+ if (options.openLocation === OpenWhere.lightbox) {
+ // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox)
+ const target = DocCast(targetDoc.annotationOn, targetDoc);
+ const contextView = this.getDocumentView(DocCast(target.embedContainer));
+ if (contextView?.docView?._componentView?.addDocTab?.(target, OpenWhere.lightbox)) {
+ await new Promise<void>(waitres => setTimeout(() => waitres()));
}
- finished?.();
+ }
+ docContextPath.shift();
+ const childViewIterator = async (docView: DocumentView) => {
+ const innerDoc = docContextPath.shift();
+ return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
};
- const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
- const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
- const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
- const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
- const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (annoContainerView) {
- if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
- annoContainerView.iconify(() =>
- annoContainerView.focus(targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoomScale,
- 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 (rootContextView) {
+ const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
+ this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
}
- if (focusView) {
- !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
- const doFocus = (forceDidFocus: boolean) =>
- focusView.focus(originalTarget ?? targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoomScale,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(forceDidFocus || didFocus);
- res(ViewAdjustment.doNothing);
- }),
- });
- if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
- focusView.iconify(() => doFocus(true));
- } else {
- doFocus(false);
- }
- } else {
- if (!targetDocContext) {
- // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
- createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
- } else {
- // otherwise try to get a view of the context of the target
- if (targetDocContextView) {
- // we found a context view and aren't forced to create a new one ... focus on the context first..
- wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
- targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
- targetDocContext._viewTransition = 'transform 500ms';
- targetDocContextView.props.focus(targetDocContextView.rootDoc, {
- willZoom,
- afterFocus: async () => {
- targetDocContext._viewTransition = undefined;
- if (targetDocContext.layoutKey === 'layout_icon') {
- targetDocContextView.iconify(() =>
- this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
- );
- }
- return ViewAdjustment.doNothing;
- },
- });
+ finished?.();
+ };
- // 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;
- targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
- finished?.();
- } else {
- // no timecode means we need to find the context view and focus on our target
- const findView = (delay: number) => {
- 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.focus(targetDoc, {
- willZoom,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- !noSelect && focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- }); // focus on the target in the context
- } else if (delay > 1000) {
- // we didn't find the target, so it must have moved out of the context. Go back to just creating it.
- if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc);
- if (targetDoc.layout) {
- // there will no layout for a TEXTANCHOR type document
- createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target
- }
- } else {
- setTimeout(() => findView(delay + 200), 200);
- }
- };
- 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, presZoomScale)
- );
- }
- }
- // 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
- );
- }
- }
+ focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => {
+ let contextView: DocumentView | undefined; // view containing context that contains target
+ while (true) {
+ docView.rootDoc.layout_fieldKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined;
+ docView.props.focus(docView.rootDoc, options); // focus the view within its container
+ const { childDocView, viewSpec } = await iterator(docView);
+ if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView };
+ contextView = docView;
+ docView = childDocView;
}
};
-}
-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.MyFilesystem && Cast(doc.context, Doc, null);
- const showDoc = context || doc;
- CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+
+ @action
+ restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: DocFocusOptions, contextView: Opt<DocumentView>, targetDoc: Doc) {
+ if (viewSpec && docView) {
+ if (docView.ComponentView instanceof FormattedTextBox) docView.ComponentView?.focus(viewSpec, options);
+ PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
+ Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
+ if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc);
+ if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden;
+ if (options.effect) docView.rootDoc[Animation] = options.effect;
+
+ if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
+ // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+ contextView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+ contextView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+ DocumentManager._overlayViews.add(contextView);
+ }
+ Doc.AddUnHighlightWatcher(() => {
+ docView.rootDoc[Animation] = undefined;
+ DocumentManager.removeOverlayViews();
+ contextView && (contextView.htmlOverlayEffect = '');
+ });
+ }
}
}
+export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+ const func = () => {
+ const cv = DocumentManager.Instance.getDocumentView(containingDoc);
+ const dv = DocumentManager.Instance.getDocumentView(doc, cv);
+ if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) {
+ DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc));
+ } else {
+ const container = DocCast(containingDoc ?? doc.embedContainer ?? doc);
+ const showDoc = !Doc.IsSystem(container) ? container : doc;
+ options.toggleTarget = undefined;
+ DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+ const cv = DocumentManager.Instance.getDocumentView(containingDoc);
+ const dv = DocumentManager.Instance.getDocumentView(doc, cv);
+ dv && Doc.linkFollowHighlight(dv.rootDoc);
+ });
+ }
+ };
+ if (doc.hidden) {
+ doc.hidden = false;
+ options.toggleTarget = false;
+ setTimeout(func);
+ } else func();
+}
ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index d0690fa10..85101fcab 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,55 +1,50 @@
import { action, observable, runInAction } from 'mobx';
import { DateField } from '../../fields/DateField';
-import { Doc, Field, Opt } from '../../fields/Doc';
+import { Doc, Field, Opt, StrListCast } 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 { BoolCast, 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 { Colors } from '../views/global/globalEnums';
import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { SelectionManager } from './SelectionManager';
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
+export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
/**
* Initialize drag
* @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> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
- const onRowMove = async (e: PointerEvent) => {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | undefined) {
+ const onRowMove = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
- const doc = await docFunc();
+ const doc = docFunc();
if (doc) {
const dragData = new DragManager.DocumentDragData([doc]);
- dragData.dropAction = dropAction;
- dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- dragStarted?.();
}
};
const onRowUp = (): void => {
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
- const onItemDown = async (e: React.PointerEvent) => {
+ const onItemDown = (e: React.PointerEvent) => {
if (e.button === 0) {
e.stopPropagation();
if (e.shiftKey) {
e.persist();
- const dragDoc = await docFunc();
+ const dragDoc = docFunc();
dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]);
} else {
document.addEventListener('pointermove', onRowMove);
@@ -130,8 +125,8 @@ export namespace DragManager {
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'
- removeDropProperties?: string[];
+ dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move'
+ dropPropertiesToRemove?: string[];
moveDocument?: MoveFunction;
removeDocument?: RemoveFunction;
isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts
@@ -149,10 +144,14 @@ export namespace DragManager {
linkDragView: DocumentView;
}
export class ColumnDragData {
- constructor(colKey: SchemaHeaderField) {
- this.colKey = colKey;
+ // constructor(colKey: SchemaHeaderField) {
+ // this.colKey = colKey;
+ // }
+ // colKey: SchemaHeaderField;
+ constructor(colIndex: number) {
+ this.colIndex = colIndex;
}
- colKey: SchemaHeaderField;
+ colIndex: number;
}
// used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made.
// this is pretty clunky and should be rethought out using linkDrag or DocumentDrag
@@ -162,7 +161,6 @@ export namespace DragManager {
this.dropDocCreator = dropDocCreator;
this.offset = [0, 0];
}
- linkSourceDoc?: Doc;
dropDocCreator: (annotationOn: Doc | undefined) => Doc;
dropDocument?: Doc;
offset: number[];
@@ -170,6 +168,13 @@ export namespace DragManager {
userDropAction: dropActionType;
}
+ let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ if (de.complete.docDragData) {
+ targetAction && (de.complete.docDragData.dropAction = targetAction);
+ e.stopPropagation();
+ }
+ };
+
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");
@@ -178,7 +183,7 @@ export namespace DragManager {
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);
+ (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.dropAction) as dropActionType);
};
element.addEventListener('dashOnDrop', handler);
doc && element.addEventListener('dashPreDrop', preDropHandler);
@@ -189,10 +194,10 @@ export namespace DragManager {
};
}
- // drag a document and drop it (or make an alias/copy on drop)
+ // drag a document and drop it (or make an embed/copy on drop)
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.author_date && (dropDoc.author_date = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
return dropDoc;
};
@@ -201,27 +206,31 @@ 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.BestAlias(d)
- : docDragData.dropAction === 'proto'
- ? Doc.GetProto(d)
- : docDragData.dropAction === 'copy'
- ? (
- await Doc.MakeClone(d)
- ).clone
- : d
+ 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 === 'embed'
+ ? Doc.BestEmbedding(d)
+ : docDragData.dropAction === 'proto'
+ ? Doc.GetProto(d)
+ : docDragData.dropAction === 'copy'
+ ? (
+ await Doc.MakeClone(d)
+ ).clone
+ : d
+ )
)
- );
+ ).filter(d => 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
+ // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any))
+ .forEach((drop: Doc, i: number) => {
+ const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove);
+ const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps));
+ [...remProps, 'dropPropertiesToRemove'].map(prop => (drop[prop] = undefined));
+ });
}
return e;
};
@@ -251,13 +260,13 @@ export namespace DragManager {
}
// drags a linker button and creates a link on drop
- export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | (() => Doc), downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.() ?? sourceView.rootDoc), downX, downY, options);
+ export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | ((addAsAnnotation: boolean) => Doc), downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.rootDoc), downX, downY, options);
}
// drags a column from a schema view
- export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], dragData, downX, downY, options);
+ export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column');
}
export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -319,10 +328,10 @@ export namespace DragManager {
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) {
+ export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) {
if (dragData.dropAction === 'none') return;
DocDragData = dragData as DocumentDragData;
- const batch = UndoManager.StartBatch('dragging');
+ const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag');
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
if (!dragDiv) {
@@ -350,7 +359,7 @@ export namespace DragManager {
top: Number.MAX_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
};
- let rot = 0;
+ let rot: number[] = [];
const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const dragElements = eles.map(ele => {
// bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element
@@ -358,9 +367,10 @@ export namespace DragManager {
// if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu)
if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
ele = ele.parentElement.parentElement.parentElement;
- rot = Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0);
+ rot.push(Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0));
} else {
useDim = true;
+ rot.push(0);
}
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
@@ -383,7 +393,7 @@ export namespace DragManager {
const rect = ele.getBoundingClientRect();
const w = ele.offsetWidth || rect.width;
const h = ele.offsetHeight || rect.height;
- const rotR = -((rot < 0 ? rot + 360 : rot) / 180) * Math.PI;
+ const rotR = -((rot.lastElement() < 0 ? rot.lastElement() + 360 : rot.lastElement()) / 180) * Math.PI;
const tl = [0, 0];
const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
@@ -417,7 +427,7 @@ export namespace DragManager {
transformOrigin: '0 0',
width,
height,
- transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
+ transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot.lastElement()}deg) scale(${scaling})`,
});
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
@@ -480,7 +490,7 @@ export namespace DragManager {
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 ? 'embed' : dragData.defaultDropAction;
}
if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header' || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
if (!startWindowDragTimer) {
@@ -503,7 +513,7 @@ export namespace DragManager {
const target = document.elementFromPoint(e.x, e.y);
- if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) {
+ if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._freeform_noAutoPan)) {
const autoScrollHandler = () => {
target.dispatchEvent(
new CustomEvent<React.DragEvent>('dashDragAutoScroll', {
@@ -558,7 +568,7 @@ export namespace DragManager {
const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
lastPt = { x, y };
- dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`));
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg) scale(${scalings[i]})`));
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
};
const upHandler = (e: PointerEvent) => {
@@ -572,6 +582,7 @@ export namespace DragManager {
async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
const dropArgs = {
+ cancelable: true, // allows preventDefault() to be called to cancel the drop
bubbles: true,
detail: {
...pos,
@@ -584,9 +595,26 @@ export namespace DragManager {
},
};
target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs));
+ UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes)
await finishDrag?.(complete);
- target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs));
+ UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called
options?.dragComplete?.(complete);
endDrag?.();
}
}
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: boolean) {
+ if (readOnly) {
+ if (SelectionManager.Views().length)
+ return SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged)
+ ? Colors.MEDIUM_BLUE
+ : SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged === false)
+ ? 'transparent'
+ : DragManager.GetRaiseWhenDragged()
+ ? Colors.MEDIUM_BLUE_ALT
+ : Colors.LIGHT_BLUE;
+ return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent';
+ }
+ if (!forAllDocs) SelectionManager.Views().map(dv => (dv.rootDoc.raiseWhenDragged ? (dv.rootDoc.raiseWhenDragged = undefined) : dv.rootDoc.raiseWhenDragged === false ? (dv.rootDoc.raiseWhenDragged = true) : (dv.rootDoc.raiseWhenDragged = false)));
+ else DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+});
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 7c209d1e0..47997cc5c 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -33,15 +33,7 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
let any = false;
docs.forEach(d => {
if (!StrCast(d.title).startsWith('-')) {
- const params = StrCast(d.title)
- .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1]
- .replace('()', '');
- if (params) {
- any = makeTemplate(d, false) || any;
- d.PARAMS = params;
- } else {
- any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
- }
+ any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
} else if (d.type === DocumentType.COL || d.data instanceof RichTextField) {
any = makeTemplate(d, false) || any;
}
@@ -65,11 +57,11 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) {
- if (data.removeDropProperties || dbox.removeDropProperties) {
- //dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon
- dbox = Doc.MakeAlias(dbox);
- const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
- const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
+ if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) {
+ //dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon
+ dbox = Doc.MakeEmbedding(dbox);
+ const dragProps = Cast(dbox.dropPropertiesToRemove, listSpec('string'), []);
+ const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps));
remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
@@ -89,7 +81,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt',
});
dbox.dragFactory = layoutDoc;
- dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
+ dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
} else if (doc.isButtonBar) {
dbox.ignoreClick = true;
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index c8b784390..da947aba6 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -155,7 +155,7 @@ export class GroupManager extends React.Component<{}> {
addGroup(groupDoc: Doc): boolean {
if (this.GroupManagerDoc) {
Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc);
- this.GroupManagerDoc['data-lastModified'] = new DateField();
+ this.GroupManagerDoc['data_modificationDate'] = new DateField();
return true;
}
return false;
@@ -176,7 +176,7 @@ export class GroupManager extends React.Component<{}> {
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_modificationDate'] = new DateField();
if (group === this.currentGroup) {
this.currentGroup = undefined;
}
@@ -197,7 +197,7 @@ 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_modificationDate'] = new DateField());
}
}
@@ -214,7 +214,7 @@ 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_modificationDate'] = new DateField());
}
}
}
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index 297520d5d..151f18d6f 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -20,7 +20,7 @@ export namespace Hypothesis {
export const getSourceWebDoc = async (uri: string) => {
const result = await findWebDoc(uri);
console.log(result ? 'existing doc found' : 'existing doc NOT found');
- return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
+ return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, data_useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
};
/**
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 37571ae01..1a4c2450e 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,27 +1,29 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { BatchedArray } from "array-batcher";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import Measure, { ContentRect } from "react-measure";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast } from "../../../fields/Types";
-import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes";
-import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
-import { DocumentManager } from "../DocumentManager";
-import "./DirectoryImportBox.scss";
-import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
-import React = require("react");
+import { BatchedArray } from 'array-batcher';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import Measure, { ContentRect } from 'react-measure';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { BoolCast, Cast, NumCast } from '../../../fields/Types';
+import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes';
+import { Utils } from '../../../Utils';
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
+import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
+import { DocumentManager } from '../DocumentManager';
+import './DirectoryImportBox.scss';
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry';
+import React = require('react');
+import _ = require('lodash');
-const unsupported = ["text/html", "text/plain"];
+const unsupported = ['text/html', 'text/plain'];
@observer
export class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -29,7 +31,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
@observable private top = 0;
@observable private left = 0;
private dimensions = 50;
- @observable private phase = "";
+ @observable private phase = '';
private disposer: Opt<IReactionDisposer>;
@observable private entries: ImportMetadataEntry[] = [];
@@ -40,7 +42,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
@observable private uploading = false;
@observable private removeHover = false;
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DirectoryImportBox, fieldKey);
+ }
constructor(props: FieldViewProps) {
super(props);
@@ -71,7 +75,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
runInAction(() => {
this.uploading = true;
- this.phase = "Initializing download...";
+ this.phase = 'Initializing download...';
});
const docs: Doc[] = [];
@@ -79,7 +83,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const files = e.target.files;
if (!files || files.length === 0) return;
- const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+ const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0];
const validated: File[] = [];
for (let i = 0; i < files.length; i++) {
@@ -100,7 +104,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const sizes: number[] = [];
const modifiedDates: number[] = [];
- runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
+ runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`));
const batched = BatchedArray.from(validated, { batchSize: 15 });
const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
@@ -108,24 +112,29 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
sizes.push(file.size);
modifiedDates.push(file.lastModified);
});
- collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
- runInAction(() => this.completed += batch.length);
+ collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file =>({file})))));
+ runInAction(() => (this.completed += batch.length));
});
- await Promise.all(uploads.map(async response => {
- const { source: { type }, result } = response;
- if (result instanceof Error) {
- return;
- }
- const { accessPaths, exifData } = result;
- const path = Utils.prepend(accessPaths.agnostic.client);
- const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 });
- const { data, error } = exifData;
- if (document) {
- Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
- docs.push(document);
- }
- }));
+ await Promise.all(
+ uploads.map(async response => {
+ const {
+ source: { type },
+ result,
+ } = response;
+ if (result instanceof Error) {
+ return;
+ }
+ const { accessPaths, exifData } = result;
+ const path = Utils.prepend(accessPaths.agnostic.client);
+ const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 }));
+ const { data, error } = exifData;
+ if (document) {
+ Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
+ docs.push(document);
+ }
+ })
+ );
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
@@ -146,22 +155,22 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
_height: 500,
_chromeHidden: true,
x: NumCast(doc.x),
- y: NumCast(doc.y) + offset
+ y: NumCast(doc.y) + offset,
};
- const parent = this.props.ContainingCollectionView;
- if (parent) {
+ const parent = this.props.DocumentView?.().props.docViewPath().lastElement();
+ if (parent?.rootDoc.type === DocumentType.COL) {
let importContainer: Doc;
if (docs.length < 50) {
importContainer = Docs.Create.MasonryDocument(docs, options);
} else {
- const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")];
+ const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')];
importContainer = Docs.Create.SchemaDocument(headers, docs, options);
}
- runInAction(() => this.phase = 'External: uploading files to Google Photos...');
+ runInAction(() => (this.phase = 'External: uploading files to Google Photos...'));
await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
- Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+ Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer);
!this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
- DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []);
+ DocumentManager.Instance.showDocument(importContainer, { willZoomCentered: true });
}
runInAction(() => {
@@ -169,14 +178,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
this.quota = 1;
this.completed = 0;
});
- }
+ };
componentDidMount() {
- this.selector.current!.setAttribute("directory", "");
- this.selector.current!.setAttribute("webkitdirectory", "");
+ this.selector.current!.setAttribute('directory', '');
+ this.selector.current!.setAttribute('webkitdirectory', '');
this.disposer = reaction(
() => this.completed,
- completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+ completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`))
);
}
@@ -193,7 +202,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const offset = this.dimensions / 2;
this.left = bounds.width / 2 - offset;
this.top = bounds.height / 2 - offset;
- }
+ };
@action
addMetadataEntry = async () => {
@@ -201,8 +210,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
entryDoc.checked = false;
entryDoc.key = keyPlaceholder;
entryDoc.value = valuePlaceholder;
- Doc.AddDocToList(this.props.Document, "data", entryDoc);
- }
+ Doc.AddDocToList(this.props.Document, 'data', entryDoc);
+ };
@action
remove = async (entry: ImportMetadataEntry) => {
@@ -217,7 +226,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
}
}
}
- }
+ };
render() {
const dimensions = 50;
@@ -228,193 +237,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const uploading = this.uploading;
const showRemoveLabel = this.removeHover;
const persistent = this.persistent;
- let percent = `${completed / quota * 100}`;
- percent = percent.split(".")[0];
- percent = percent.startsWith("100") ? "99" : percent;
+ let percent = `${(completed / quota) * 100}`;
+ percent = percent.split('.')[0];
+ percent = percent.startsWith('100') ? '99' : percent;
const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
- const message = <span className={"phase"}>{this.phase}</span>;
- const centerPiece = this.phase.includes("Google Photos") ?
- <img src={"/assets/google_photos.png"} style={{
- transition: "0.4s opacity ease",
- width: 30,
- height: 30,
- opacity: uploading ? 1 : 0,
- pointerEvents: "none",
- position: "absolute",
- left: 12,
- top: this.top + 10,
- fontSize: 18,
- color: "white",
- marginLeft: this.left + marginOffset
- }} />
- : <div
+ const message = <span className={'phase'}>{this.phase}</span>;
+ const centerPiece = this.phase.includes('Google Photos') ? (
+ <img
+ src={'/assets/google_photos.png'}
style={{
- transition: "0.4s opacity ease",
+ transition: '0.4s opacity ease',
+ width: 30,
+ height: 30,
opacity: uploading ? 1 : 0,
- pointerEvents: "none",
- position: "absolute",
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: 12,
+ top: this.top + 10,
+ fontSize: 18,
+ color: 'white',
+ marginLeft: this.left + marginOffset,
+ }}
+ />
+ ) : (
+ <div
+ style={{
+ transition: '0.4s opacity ease',
+ opacity: uploading ? 1 : 0,
+ pointerEvents: 'none',
+ position: 'absolute',
left: 10,
top: this.top + 12.3,
fontSize: 18,
- color: "white",
- marginLeft: this.left + marginOffset
- }}>{percent}%</div>;
+ color: 'white',
+ marginLeft: this.left + marginOffset,
+ }}>
+ {percent}%
+ </div>
+ );
return (
<Measure offset onResize={this.preserveCentering}>
- {({ measureRef }) =>
- <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ {({ measureRef }) => (
+ <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
{message}
<input
- id={"selector"}
+ id={'selector'}
ref={this.selector}
onChange={this.handleSelection}
type="file"
style={{
- position: "absolute",
- display: "none"
- }} />
+ position: 'absolute',
+ display: 'none',
+ }}
+ />
<label
- htmlFor={"selector"}
+ htmlFor={'selector'}
style={{
opacity: isEditing ? 0 : 1,
- pointerEvents: isEditing ? "none" : "all",
- transition: "0.4s ease opacity"
- }}
- >
- <div style={{
- width: dimensions,
- height: dimensions,
- borderRadius: "50%",
- background: "black",
- position: "absolute",
- left: this.left,
- top: this.top
- }} />
- <div style={{
- position: "absolute",
- left: this.left + 8,
- top: this.top + 10,
- opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ pointerEvents: isEditing ? 'none' : 'all',
+ transition: '0.4s ease opacity',
}}>
- <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} />
+ <div
+ style={{
+ width: dimensions,
+ height: dimensions,
+ borderRadius: '50%',
+ background: 'black',
+ position: 'absolute',
+ left: this.left,
+ top: this.top,
+ }}
+ />
+ <div
+ style={{
+ position: 'absolute',
+ left: this.left + 8,
+ top: this.top + 10,
+ opacity: uploading ? 0 : 1,
+ transition: '0.4s opacity ease',
+ }}>
+ <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} />
</div>
<img
style={{
width: 80,
height: 80,
- transition: "0.4s opacity ease",
+ transition: '0.4s opacity ease',
opacity: uploading ? 0.7 : 0,
- position: "absolute",
+ position: 'absolute',
top: this.top - 15,
- left: this.left - 15
+ left: this.left - 15,
}}
- src={"/assets/loading.gif"}></img>
+ src={'/assets/loading.gif'}></img>
</label>
<input
- type={"checkbox"}
- onChange={e => runInAction(() => this.persistent = e.target.checked)}
+ type={'checkbox'}
+ onChange={e => runInAction(() => (this.persistent = e.target.checked))}
style={{
margin: 0,
- position: "absolute",
+ position: 'absolute',
left: 10,
bottom: 10,
opacity: isEditing || uploading ? 0 : 1,
- transition: "0.4s opacity ease",
- pointerEvents: isEditing || uploading ? "none" : "all"
+ transition: '0.4s opacity ease',
+ pointerEvents: isEditing || uploading ? 'none' : 'all',
}}
checked={this.persistent}
- onPointerEnter={action(() => this.removeHover = true)}
- onPointerLeave={action(() => this.removeHover = false)}
+ onPointerEnter={action(() => (this.removeHover = true))}
+ onPointerLeave={action(() => (this.removeHover = false))}
/>
<p
style={{
- position: "absolute",
+ position: 'absolute',
left: 27,
bottom: 8.4,
fontSize: 12,
opacity: showRemoveLabel ? 1 : 0,
- transition: "0.4s opacity ease"
- }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+ transition: '0.4s opacity ease',
+ }}>
+ Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload
+ </p>
{centerPiece}
<div
style={{
- position: "absolute",
+ position: 'absolute',
top: 10,
right: 10,
- borderRadius: "50%",
+ borderRadius: '50%',
width: 25,
height: 25,
- background: "black",
- pointerEvents: uploading ? "none" : "all",
+ background: 'black',
+ pointerEvents: uploading ? 'none' : 'all',
opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ transition: '0.4s opacity ease',
}}
- title={isEditing ? "Back to Upload" : "Add Metadata"}
- onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+ title={isEditing ? 'Back to Upload' : 'Add Metadata'}
+ onClick={action(() => (this.editingMetadata = !this.editingMetadata))}
/>
<FontAwesomeIcon
style={{
- pointerEvents: "none",
- position: "absolute",
+ pointerEvents: 'none',
+ position: 'absolute',
right: isEditing ? 14 : 15,
top: isEditing ? 15.4 : 16,
opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ transition: '0.4s opacity ease',
}}
- icon={isEditing ? "cloud-upload-alt" : "tag"}
+ icon={isEditing ? 'cloud-upload-alt' : 'tag'}
color="#FFFFFF"
- size={"1x"}
+ size={'1x'}
/>
<div
style={{
- transition: "0.4s ease opacity",
- width: "100%",
- height: "100%",
- pointerEvents: isEditing ? "all" : "none",
+ transition: '0.4s ease opacity',
+ width: '100%',
+ height: '100%',
+ pointerEvents: isEditing ? 'all' : 'none',
opacity: isEditing ? 1 : 0,
- overflowY: "scroll"
- }}
- >
+ overflowY: 'scroll',
+ }}>
<div
style={{
- borderRadius: "50%",
+ borderRadius: '50%',
width: 25,
height: 25,
marginLeft: 10,
- position: "absolute",
+ position: 'absolute',
right: 41,
- top: 10
+ top: 10,
}}
- title={"Add Metadata Entry"}
- onClick={this.addMetadataEntry}
- >
+ title={'Add Metadata Entry'}
+ onClick={this.addMetadataEntry}>
<FontAwesomeIcon
style={{
- pointerEvents: "none",
+ pointerEvents: 'none',
marginLeft: 6.4,
- marginTop: 5.2
+ marginTop: 5.2,
}}
- icon={"plus"}
- size={"1x"}
+ icon={'plus'}
+ size={'1x'}
/>
</div>
- <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
- <hr style={{ margin: "6px 10px 12px 10px" }} />
- {entries.map(doc =>
+ <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p>
+ <hr style={{ margin: '6px 10px 12px 10px' }} />
+ {entries.map(doc => (
<ImportMetadataEntry
Document={doc}
key={doc[Id]}
remove={this.remove}
- ref={(el) => { if (el) this.entries.push(el); }}
+ ref={el => {
+ if (el) this.entries.push(el);
+ }}
next={this.addMetadataEntry}
/>
- )}
+ ))}
</div>
</div>
- }
+ )}
</Measure>
);
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 9bd92a316..55d37f544 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -1,12 +1,11 @@
-import { Doc } from "../../../fields/Doc";
-import { ImageField } from "../../../fields/URLField";
-import { Cast, StrCast, NumCast } from "../../../fields/Types";
-import { Networking } from "../../Network";
-import { Id } from "../../../fields/FieldSymbols";
-import { Utils } from "../../../Utils";
+import { Doc } from '../../../fields/Doc';
+import { ImageField } from '../../../fields/URLField';
+import { Cast, StrCast, NumCast } from '../../../fields/Types';
+import { Networking } from '../../Network';
+import { Id } from '../../../fields/FieldSymbols';
+import { Utils } from '../../../Utils';
export namespace ImageUtils {
-
export const ExtractExif = async (document: Doc): Promise<boolean> => {
const field = Cast(document.data, ImageField);
if (!field) {
@@ -17,23 +16,22 @@ export namespace ImageUtils {
contentSize,
nativeWidth,
nativeHeight,
- exifData: { error, data }
- } = await Networking.PostToServer("/inspectImage", { source });
+ exifData: { error, data },
+ } = await Networking.PostToServer('/inspectImage', { source });
document.exif = error || Doc.Get.FromJson({ data });
const proto = Doc.GetProto(document);
- nativeWidth && (document._height = NumCast(document._width) * nativeHeight / nativeWidth);
- proto["data-nativeWidth"] = nativeWidth;
- proto["data-nativeHeight"] = nativeHeight;
- proto["data-path"] = source;
- proto.contentSize = contentSize ? contentSize : undefined;
+ nativeWidth && (document._height = (NumCast(document._width) * nativeHeight) / nativeWidth);
+ proto['data_nativeWidth'] = nativeWidth;
+ proto['data_nativeHeight'] = nativeHeight;
+ proto['data-path'] = source;
+ proto.data_contentSize = contentSize ? contentSize : undefined;
return data !== undefined;
};
export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
- const a = document.createElement("a");
+ const a = document.createElement('a');
a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`);
a.download = `Dash Export [${StrCast(collection.title)}].zip`;
a.click();
};
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 3cdf4dbd2..043f0f1f3 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -107,7 +107,8 @@ export namespace InteractionUtils {
pevents: string,
opacity: number,
nodefs: boolean,
- downHdlr?: (e: React.PointerEvent) => void
+ downHdlr?: (e: React.PointerEvent) => void,
+ mask?: boolean
) {
const pts = shape ? makePolygon(shape, points) : points;
@@ -133,6 +134,11 @@ export namespace InteractionUtils {
{/* setting the svg fill sets the arrowStart fill */}
{nodefs ? null : (
<defs>
+ {!mask ? null : (
+ <filter id={`mask${defGuid}`} x="-1" y="-1" width="500%" height="500%">
+ <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5"></feGaussianBlur>
+ </filter>
+ )}
{arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
<marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
<circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
@@ -155,6 +161,7 @@ export namespace InteractionUtils {
<polygon
style={{ stroke: color }}
strokeLinejoin={lineJoin as any}
+ strokeDasharray={dashArray}
strokeWidth={(markerStrokeWidth * 2) / 3}
points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
/>
@@ -168,6 +175,7 @@ export namespace InteractionUtils {
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill && fill !== 'transparent' ? fill : 'none',
+ filter: mask ? `url(#mask${defGuid})` : undefined,
opacity: 1.0,
// opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents as any,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f75ac24f5..f74409e42 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,19 +1,19 @@
-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 { action, observable, runInAction } from 'mobx';
+import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc';
+import { ScriptField } from '../../fields/ScriptField';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DocFocusOptions, OpenWhere } from '../views/nodes/DocumentView';
+import { PresBox } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
+import { LinkManager } from './LinkManager';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
/*
* link doc:
- * - anchor1: doc
- * - anchor2: doc
+ * - link_anchor_1: doc
+ * - link_anchor_2: doc
*
* group doc:
* - type: string representing the group type/name/category
@@ -23,90 +23,114 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
* - user defined kvps
*/
export class LinkFollower {
+ @observable public static IsFollowing = false;
// 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 FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
+ const batch = UndoManager.StartBatch('Follow Link');
+ runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
+ LinkFollower.traverseLink(
+ linkDoc,
+ sourceDoc,
+ action(() => {
+ batch.end();
+ Doc.AddUnHighlightWatcher(action(() => (LinkFollower.IsFollowing = false)));
+ }),
+ 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);
+ public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
+ const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc));
+ const isAnchor = (sourceDoc: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, sourceDoc) || Doc.AreProtosEqual(anchor.annotationOn as Doc, sourceDoc);
+ const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
+ const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1
+ const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2
+ const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2)));
+ const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1)));
+ const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
+ const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks;
+ const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
var count = 0;
const allFinished = () => ++count === followLinks.length && finished?.();
+ if (!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 target = (
+ sourceDoc === linkDoc.link_anchor_1
+ ? linkDoc.link_anchor_2
+ : sourceDoc === linkDoc.link_anchor_2
+ ? linkDoc.link_anchor_1
+ : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc)
+ ? linkDoc.link_anchor_2
+ : linkDoc.link_anchor_1
+ ) as Doc;
+ if (target) {
+ const doFollow = (canToggle?: boolean) => {
+ const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
+ const options: DocFocusOptions = {
+ playAudio: BoolCast(sourceDoc.followLinkAudio),
+ toggleTarget,
+ noSelect: true,
+ willPan: true,
+ willZoomCentered: BoolCast(sourceDoc.followLinkZoom, false),
+ zoomTime: NumCast(sourceDoc.followLinkTransitionTime, 500),
+ zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
+ easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
+ openLocation: StrCast(sourceDoc.followLinkLocation, OpenWhere.lightbox) as OpenWhere,
+ effect: sourceDoc,
+ zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
+ };
+ if (target.type === DocumentType.PRES) {
+ const containerDocContext = DocumentManager.GetContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail
+ SelectionManager.DeselectAll();
+ if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext))) {
+ PresBox.OpenPresMinimized(target, [0, 0]);
}
- 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);
+ finished?.();
+ } else {
+ DocumentManager.Instance.showDocument(target, options, allFinished);
+ }
+ };
+ let movedTarget = false;
+ if (sourceDoc.followLinkLocation === OpenWhere.inParent) {
+ const sourceDocParent = DocCast(sourceDoc.embedContainer);
+ if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) {
+ Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutFieldKey(target.embedContainer), target);
+ movedTarget = true;
}
- } else {
- allFinished();
- }
+ if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) {
+ Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
+ movedTarget = true;
+ }
+ target.embedContainer = sourceDocParent;
+ const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
+ if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
+ target.x = moveTo[0];
+ movedTarget = true;
+ }
+ if (sourceDoc.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
+ target.y = moveTo[1];
+ movedTarget = true;
+ }
+ if (movedTarget) setTimeout(doFollow);
+ else doFollow(true);
+ } else doFollow(true);
} else {
allFinished();
}
});
}
}
+
+ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
+ SelectionManager.DeselectAll();
+ LinkFollower.FollowLink(undefined, doc, altKey);
+});
+
+export function FollowLinkScript() {
+ return ScriptField.MakeScript('followLink(this,altKey)', { altKey: 'boolean' });
+}
+export function IsFollowLinkScript(field: FieldResult<Field>) {
+ return ScriptCast(field)?.script.originalScript.includes('followLink(');
+}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 7a12a8580..ce422f849 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,13 +1,16 @@
import { action, observable, observe } from 'mobx';
import { computedFn } from 'mobx-utils';
-import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import { DirectLinks } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
-import { Cast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { DocServer } from '../DocServer';
+import { ScriptingGlobals } from './ScriptingGlobals';
/*
* link doc:
- * - anchor1: doc
- * - anchor2: doc
+ * - link_anchor_1: doc
+ * - link_anchor_2: doc
*
* group doc:
* - type: string representing the group type/name/category
@@ -19,85 +22,100 @@ import { Cast, StrCast } from '../../fields/Types';
export class LinkManager {
@observable static _instance: LinkManager;
@observable static userLinkDBs: Doc[] = [];
- public static currentLink: Opt<Doc>;
+ @observable public static currentLink: Opt<Doc>;
+ @observable public static currentLinkAnchor: Opt<Doc>;
public static get Instance() {
return LinkManager._instance;
}
- public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+
+ public static Links(doc: Doc | undefined) {
+ return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : [];
+ }
+ public static addLinkDB = async (linkDb: any) => {
+ await Promise.all(
+ ((await DocListCastAsync(linkDb.data)) ?? []).map(link =>
+ // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager
+ [PromiseValue(link?.link_anchor_1), PromiseValue(link?.link_anchor_2)]
+ )
+ );
+ LinkManager.userLinkDBs.push(linkDb);
+ };
public static AutoKeywords = 'keywords:Usages';
- static links: Doc[] = [];
+ static _links: Doc[] = [];
constructor() {
LinkManager._instance = this;
- this.createLinkrelationshipLists();
+ this.createlink_relationshipLists();
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);
- }
- })
- );
+ Promise.all([link]).then(linkdoc => {
+ const link = DocCast(linkdoc[0]);
+ Promise.all([link.proto]).then(linkproto => {
+ Promise.all([link.link_anchor_1, link.link_anchor_2]).then(linkdata => {
+ const a1 = DocCast(linkdata[0]);
+ const a2 = DocCast(linkdata[1]);
+ a1 &&
+ a2 &&
+ Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then(
+ action(protos => {
+ (protos[0] as Doc)?.[DirectLinks].add(link);
+ (protos[1] as Doc)?.[DirectLinks].add(link);
+ })
+ );
+ });
+ });
});
};
const remLinkFromDoc = (link: Doc) => {
- const a1 = link?.anchor1;
- const a2 = link?.anchor2;
+ const a1 = link?.link_anchor_1;
+ const a2 = link?.link_anchor_2;
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);
+ Doc.GetProto(a1)[DirectLinks].delete(link);
+ Doc.GetProto(a2)[DirectLinks].delete(link);
+ Doc.GetProto(link)[DirectLinks].delete(link);
}
})
);
};
const watchUserLinkDB = (userLinkDBDoc: Doc) => {
- LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
- const toRealField = (field: Field) => (field instanceof ProxyField ? field.value() : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
- observe(
- userLinkDBDoc.data as Doc,
- change => {
- // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
- switch (change.type as any) {
- case 'splice':
- (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
- (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- break;
- case 'update': //let oldValue = change.oldValue;
- }
- },
- true
- );
- observe(
- userLinkDBDoc,
- 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
- change => {
- switch (change.type as any) {
- case 'update':
- Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => {
- const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length);
- const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length);
+ 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
+ if (userLinkDBDoc.data) {
+ observe(
+ userLinkDBDoc.data,
+ change => {
+ // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
+ switch (change.type as any) {
+ case 'splice':
+ (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ break;
+ case 'update': //let oldValue = change.oldValue;
+ }
+ },
+ true
+ );
+ observe(
+ userLinkDBDoc,
+ 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+ change => {
+ switch (change.type as any) {
+ case 'update':
+ Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => {
+ const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length);
+ const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length);
- const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
- const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
- added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
- removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- });
- }
- },
- true
- );
+ 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,
@@ -112,25 +130,19 @@ export class LinkManager {
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 = () => {
+ public createlink_relationshipLists = () => {
//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>());
+ !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>());
+ !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>());
+ !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>());
};
public addLink(linkDoc: Doc, checkExists = false) {
if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
+ setTimeout(DocServer.UPDATE_SERVER_CACHE, 100);
}
}
public deleteLink(linkDoc: Doc) {
@@ -144,37 +156,27 @@ export class LinkManager {
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]) {
- return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
- } else {
- return [];
- }
+ return Array.from(Doc.GetProto(anchor)[DirectLinks] ?? []);
} // finds all links that contain the given anchor
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');
return [];
}
- const dirLinks = Doc.GetProto(anchor)[DirectLinksSym];
- 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());
- return related;
+ const dirLinks = Doc.GetProto(anchor)[DirectLinks];
+ const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']);
+ if (!annos) debugger;
+ return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
}, true);
// returns map of group type to anchor's links in that group type
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 relation = StrCast(link.linkRelationship);
- const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation;
+ if (link.link_relationship && link.link_relationship !== '-ungrouped-') {
+ const relation = StrCast(link.link_relationship);
+ const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation;
const group = anchorGroups.get(anchorRelation);
anchorGroups.set(anchorRelation, group ? [...group, link] : [link]);
} else {
@@ -190,12 +192,18 @@ export class LinkManager {
//TODO This should probably return undefined if there isn't an opposite anchor
//TODO This should also await the return value of the anchor so we don't filter out promises
public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
- const a1 = Cast(linkDoc.anchor1, Doc, null);
- const a2 = Cast(linkDoc.anchor2, Doc, null);
- if (Doc.AreProtosEqual(anchor, a1)) return a2;
- if (Doc.AreProtosEqual(anchor, a2)) return a1;
- if (Doc.AreProtosEqual(anchor, a1.annotationOn as Doc)) return a2;
- if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1;
+ const a1 = Cast(linkDoc.link_anchor_1, Doc, null);
+ const a2 = Cast(linkDoc.link_anchor_2, Doc, null);
+ if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1.annotationOn, a1))) return a2;
+ if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2.annotationOn, a2))) return a1;
if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
}
+
+ScriptingGlobals.add(
+ function links(doc: any) {
+ return new List(LinkManager.Links(doc));
+ },
+ 'returns all the links to the document or its annotations',
+ '(doc: any)'
+);
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
new file mode 100644
index 000000000..247267710
--- /dev/null
+++ b/src/client/util/RTFMarkup.tsx
@@ -0,0 +1,137 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { MainViewModal } from '../views/MainViewModal';
+
+@observer
+export class RTFMarkup extends React.Component<{}> {
+ static Instance: RTFMarkup;
+ @observable private isOpen = false; // whether the SharingManager modal is open or not
+
+ // private get linkVisible() {
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
+ // }
+
+ @action
+ public open = () => (this.isOpen = true);
+
+ @action
+ public close = () => (this.isOpen = false);
+
+ constructor(props: {}) {
+ super(props);
+ RTFMarkup.Instance = this;
+ }
+
+ @observable _stats: { [key: string]: any } | undefined;
+
+ /**
+ * @returns the main interface of the SharingManager.
+ */
+ @computed get cheatSheet() {
+ return (
+ <div style={{ textAlign: 'initial', height: '100%' }}>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
+ {` display wikipedia page for entered text (terminate with carriage return)`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+ {` add hashtag metadata to document. e.g, #idea`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#, ## ... ###### `}</b>
+ {` set heading style based on number of '#'s between 1 and 6`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+ {` add hashtag metadata to document. e.g, #idea`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`>> `}</b>
+ {` add a sidebar text document inline`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`\`\` `}</b>
+ {` create a code snippet block`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-f `}</b>
+ {` collapse to an inline footnote)`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-e `}</b>
+ {` collapse to elided text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-[ `}</b>
+ {` left justify text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-\\ `}</b>
+ {` center text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-] `}</b>
+ {` right justify text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%% `}</b>
+ {` restore default styling`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%color `}</b>
+ {` changes text color styling. e.g., %green.`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%num `}</b>
+ {` set font size. e.g., %10 for 10pt font`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%eq `}</b>
+ {` creates an equation block for typeset math`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%/ `}</b>
+ {` switch between primary and alternate text (see bottom right Button for hover options).`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%> `}</b>
+ {` create a bockquote section. Terminate with 2 carriage returns`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%q `}</b>
+ {` start a quoted block of text that’s indented on the left and right. Terminate with %q`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%d `}</b>
+ {` start a block text where the first line is indented`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%h `}</b>
+ {` start a block of text that begins with a hanging indent`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[:doctitle]] `}</b>
+ {` hyperlink to document specified by it’s title`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname]] `}</b>
+ {` display value of fieldname`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname=value]] `}</b>
+ {` assign value to fieldname of document and display it`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname:doctitle]] `}</b>
+ {` show value of fieldname from doc specified by it’s title`}
+ </p>
+ </div>
+ );
+ }
+
+ render() {
+ return <MainViewModal contents={this.cheatSheet} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+ }
+}
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 86bc4c5de..cbc465d6a 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,22 +1,23 @@
-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";
+import { IReactionDisposer, observable, reaction } from 'mobx';
+import { Doc, IdToDoc } from '../../fields/Doc';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { OpenWhereMod } from '../views/nodes/DocumentView';
+import { VideoBox } from '../views/nodes/VideoBox';
+import { DocumentManager } from './DocumentManager';
+import { Movement, Presentation } from './TrackMovements';
export class ReplayMovements {
- private timers: NodeJS.Timeout[] | null;
+ 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 }
+ static get Instance(): ReplayMovements {
+ return ReplayMovements._instance;
+ }
constructor() {
// init the global instance
ReplayMovements._instance = this;
@@ -37,127 +38,120 @@ export class ReplayMovements {
}
Doc.UserDoc().presentationMode = 'none';
- this.isPlaying = false
+ this.isPlaying = false;
// TODO: set userdoc presentMode to browsing
- this.timers?.map(timer => clearTimeout(timer))
- }
+ 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(); }
-
+ 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';
+ if (presentation == null) {
+ console.warn('setVideoBox on null videoBox presentation');
+ return;
}
-
- this.videoBoxDisposeFunc =
- reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
- ({ playing, timeViewed }) =>
- playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
- );
+ this.loadPresentation(presentation);
+
+ this.videoBoxDisposeFunc = reaction(
+ () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+ ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, timeViewed) : this.pauseMovements())
+ );
this.videoBox = videoBox;
- }
+ };
removeVideoBox = () => {
- if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+ 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) => {
+ loadPresentation = (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`;
+ movements.forEach((movement, i) => {
+ if (typeof movement.doc === 'string') {
+ movements[i].doc = IdToDoc(movement.doc);
+ if (!movements[i].doc) {
+ console.log('ERROR: tracked doc not found');
+ }
}
- 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; }
- }
+ getCollectionFFView = (doc: Doc) => {
+ const isInView = DocumentManager.Instance.getDocumentView(doc);
+ 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}`)
+ openTab = (doc: Doc) => {
+ if (doc === undefined) {
+ console.error(`doc undefined`);
return undefined;
}
// console.log('openTab', docId, doc);
- CollectionDockingView.AddSplit(doc, 'right');
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.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;
- }
+ document.Document._freeform_panX = panX;
+ document.Document._freeform_panY = panY;
+ };
- getFirstMovements = (movements: Movement[]): Map<string, Movement> => {
+ getFirstMovements = (movements: Movement[]): Map<Doc, Movement> => {
if (movements === null) return new Map();
// generate a set of all unique docIds
- const docIdtoFirstMove = new Map();
+ const docIdtoFirstMove = new Map<Doc, Movement>();
for (const move of movements) {
- const { docId } = move;
- if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
+ if (!docIdtoFirstMove.has(move.doc)) docIdtoFirstMove.set(move.doc, move);
}
return docIdtoFirstMove;
- }
+ };
endPlayingPresentation = () => {
this.isPlaying = false;
Doc.UserDoc().presentationMode = 'none';
- }
+ };
- public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
+ public playMovements = (presentation: Presentation, 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 (presentation.movements === null || presentation.movements.length === 0) {
+ //|| this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data');
}
if (this.isPlaying) return;
@@ -165,35 +159,34 @@ export class ReplayMovements {
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 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);
+ const isClosed = this.getCollectionFFView(firstMovement.doc) === undefined;
+ if (isClosed) this.openTab(firstMovement.doc);
// 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);
+ for (const [doc, firstMove] of docIdtoFirstMove) {
+ const colFFView = this.getCollectionFFView(doc);
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
+ const timeDiff = movement.time - timeViewed * 1000;
return setTimeout(() => {
- const collectionFFView = this.getCollectionFFView(movement.docId);
+ const collectionFFView = this.getCollectionFFView(movement.doc);
if (collectionFFView) {
this.zoomAndPan(movement, collectionFFView);
} else {
// tab wasn't open - open it and play the movement
- const openedColFFView = this.openTab(movement.docId, docIdtoDoc);
+ const openedColFFView = this.openTab(movement.doc);
console.log('openedColFFView', openedColFFView);
openedColFFView && this.zoomAndPan(movement, openedColFFView);
}
@@ -204,5 +197,5 @@ export class ReplayMovements {
}
}, timeDiff);
});
- }
+ };
}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
index 51742d455..4c1020455 100644
--- a/src/client/util/ReportManager.tsx
+++ b/src/client/util/ReportManager.tsx
@@ -173,7 +173,7 @@ export class ReportManager extends React.Component<{}> {
// upload the files to the server
if (input.files && input.files.length !== 0) {
const fileArray: File[] = Array.from(input.files);
- (Networking.UploadFilesToServer(fileArray)).then(links => {
+ (Networking.UploadFilesToServer(fileArray.map(file =>({file})))).then(links => {
console.log('finshed uploading', links.map(this.getServerPath));
this.setFileLinks((links ?? []).map(this.getServerPath));
})
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 6dcdcb71b..70c2e3842 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,7 +7,7 @@
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import * as ts from 'typescript';
import { Doc, Field } from '../../fields/Doc';
-import { ObjectField } from '../../fields/ObjectField';
+import { RefField } from '../../fields/RefField';
import { ScriptField } from '../../fields/ScriptField';
import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
@@ -58,7 +58,14 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
// let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
// let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
// let params: any[] = [Docs, ...fieldTypes];
- const compiledFunction = new Function(...paramNames, `return ${script}`);
+ const compiledFunction = (() => {
+ try {
+ return new Function(...paramNames, `return ${script}`);
+ } catch {
+ return undefined;
+ }
+ })();
+ if (!compiledFunction) return { compiled: false, errors };
const { capturedVariables = {} } = options;
const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
const argsArray: any[] = [];
@@ -153,7 +160,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser };
export type Transformer = {
transformer: ts.TransformerFactory<ts.SourceFile>;
- getVars?: () => { capturedVariables: { [name: string]: Field } };
+ getVars?: () => { [name: string]: Field };
};
export interface ScriptOptions {
requiredType?: string; // does function required a typed return value
@@ -180,7 +187,11 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const captured = options.capturedVariables ?? {};
- const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, '');
+ const signature = Object.keys(captured).reduce((p, v) => {
+ const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`;
+ if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture);
+ return p + formatCapture(captured[v]);
+ }, '');
const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
if (found) return found as CompiledScript;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
@@ -199,16 +210,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (options.transformer) {
const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
const result = ts.transform(sourceFile, [options.transformer.transformer]);
- if (options.transformer.getVars) {
- const newCaptures = options.transformer.getVars();
+ const newCaptures = options.transformer.getVars?.();
+ if (Object.keys(newCaptures ?? {}).length) {
// tslint:disable-next-line: prefer-object-spread
- options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any;
+ //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any;
+
+ const transformed = result.transformed;
+ const printer = ts.createPrinter({
+ newLine: ts.NewLineKind.LineFeed,
+ });
+ script = printer.printFile(transformed[0]);
}
- const transformed = result.transformed;
- const printer = ts.createPrinter({
- newLine: ts.NewLineKind.LineFeed,
- });
- script = printer.printFile(transformed[0]);
result.dispose();
}
const paramNames: string[] = [];
@@ -248,8 +260,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
ScriptingGlobals.resetScriptingGlobals();
}
!signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
- //console.log('COMPILED: ' + script + ':' + signature);
return result;
}
ScriptingGlobals.add(CompileScript);
+ScriptingGlobals.add(function runScript(self: Doc, script: ScriptField) {
+ return script?.script.run({ this: self, self: self }).result;
+});
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 79759a71d..d154c48a4 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -24,51 +24,57 @@ export namespace SearchUtil {
export interface SearchParams {
hl?: string;
- "hl.fl"?: string;
+ 'hl.fl'?: string;
start?: number;
rows?: number;
fq?: string;
sort?: string;
- allowAliases?: boolean;
- onlyAliases?: boolean;
- "facet"?: string;
- "facet.field"?: string;
+ allowEmbeddings?: boolean;
+ onlyEmbeddings?: boolean;
+ facet?: string;
+ 'facet.field'?: string;
}
export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>;
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
- query = query || "*"; //If we just have a filter query, search for * as the query
- const rpquery = Utils.prepend("/dashsearch");
+ query = query || '*'; //If we just have a filter query, search for * as the query
+ const rpquery = Utils.prepend('/dashsearch');
let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`);
- if (options.onlyAliases) {
- const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery;
+ if (options.onlyEmbeddings) {
+ const header = query.match(/_[atnb]?:/) ? replacedQuery : 'DEFAULT:' + replacedQuery;
replacedQuery = `{!join from=id to=proto_i}* AND ${header}`;
}
//console.log("Q: " + replacedQuery + " fq: " + options.fq);
const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } });
- const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
+ const result: IdSearchResult = gotten.startsWith('<') ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
return result;
}
const { ids, highlighting } = result;
- const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
- qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, "") }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures
- }));
+ const txtresult =
+ query !== '*' &&
+ JSON.parse(
+ await rp.get(Utils.prepend('/textsearch'), {
+ qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, '') }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures
+ })
+ );
const fileids = txtresult ? txtresult.ids : [];
const newIds: string[] = [];
const newLines: string[][] = [];
- if (fileids) {
- await Promise.all(fileids.map(async (tr: string, i: number) => {
- const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
- newIds.push(...docResult.ids);
- newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
- }));
- }
-
+ // bcz: we stopped storing fileUpload id's, so this won't find anything
+ // if (fileids) {
+ // await Promise.all(
+ // fileids.map(async (tr: string, i: number) => {
+ // const docQuery = 'fileUpload_t:' + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+ // const docResult = JSON.parse(await rp.get(Utils.prepend('/dashsearch'), { qs: { ...options, q: docQuery } }));
+ // newIds.push(...docResult.ids);
+ // newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
+ // })
+ // );
+ // }
const theDocs: Doc[] = [];
const theLines: string[][] = [];
@@ -86,7 +92,7 @@ export namespace SearchUtil {
const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
for (let i = 0; i < ids.length; i++) {
const testDoc = docs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowEmbeddings || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
theDocs.push(testDoc);
theLines.push([]);
} else {
@@ -97,48 +103,46 @@ export namespace SearchUtil {
return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines };
}
- export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
- export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
- export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
+ export async function GetEmbeddingsOfDocument(doc: Doc): Promise<Doc[]>;
+ export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
+ export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
const proto = Doc.GetProto(doc);
const protoId = proto[Id];
if (returnDocs) {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).docs;
+ return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).docs;
} else {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).ids;
+ return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).ids;
}
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
- const results = await Search("", true, { fq: `proto_i:"${doc[Id]}"` });
+ const results = await Search('', true, { fq: `proto_i:"${doc[Id]}"` });
return results.docs;
}
- export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
- const docContexts = (await Search("", true, { fq: `data_l:"${doc[Id]}"` })).docs;
- const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search("", true, { fq: `data_l:"${doc}"` }))));
- const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] };
- aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs));
+ export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[]; embeddingContexts: Doc[] }> {
+ const docContexts = (await Search('', true, { fq: `data_l:"${doc[Id]}"` })).docs;
+ const embeddings = await GetEmbeddingsOfDocument(doc, false);
+ const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', true, { fq: `data_l:"${doc}"` })));
+ const contexts = { contexts: docContexts, embeddingContexts: [] as Doc[] };
+ embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.docs));
return contexts;
}
- export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> {
- const docContexts = (await Search("", false, { fq: `data_l:"${doc[Id]}"` })).ids;
- const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search("", false, { fq: `data_l:"${doc}"` }))));
- const contexts = { contexts: docContexts, aliasContexts: [] as string[] };
- aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids));
+ export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[]; embeddingContexts: string[] }> {
+ const docContexts = (await Search('', false, { fq: `data_l:"${doc[Id]}"` })).ids;
+ const embeddings = await GetEmbeddingsOfDocument(doc, false);
+ const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', false, { fq: `data_l:"${doc}"` })));
+ const contexts = { contexts: docContexts, embeddingContexts: [] as string[] };
+ embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.ids));
return contexts;
}
export async function GetAllDocs() {
- const query = "*";
+ const query = '*';
const response = await rp.get(Utils.prepend('/dashsearch'), {
- qs:
- { start: 0, rows: 10000, q: query },
-
+ qs: { start: 0, rows: 10000, q: query },
});
const result: IdSearchResult = JSON.parse(response);
const { ids, numFound, highlighting } = result;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a3d6f5227..0125331ec 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,14 +2,16 @@ 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 { CollectionViewType } from '../documents/DocumentTypes';
import { DocumentView } from '../views/nodes/DocumentView';
+import { LinkManager } from './LinkManager';
import { ScriptingGlobals } from './ScriptingGlobals';
export namespace SelectionManager {
class Manager {
@observable IsDragging: boolean = false;
- SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ SelectedViewsMap: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ @observable SelectedViews: DocumentView[] = [];
@observable SelectedSchemaDocument: Doc | undefined;
@action
@@ -19,42 +21,51 @@ export namespace SelectionManager {
@action
SelectView(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
+ if (!manager.SelectedViewsMap.get(docView)) {
if (!ctrlPressed) {
+ if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
+ LinkManager.currentLink = undefined;
+ }
this.DeselectAll();
}
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
docView.props.whenChildContentsActiveChanged(true);
- } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) {
- Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
+ } else if (!ctrlPressed && (Array.from(manager.SelectedViewsMap.entries()).length > 1 || manager.SelectedSchemaDocument)) {
+ Array.from(manager.SelectedViewsMap.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
manager.SelectedSchemaDocument = undefined;
- manager.SelectedViews.clear();
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.length = 0;
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
}
}
@action
- DeselectView(docView: DocumentView): void {
- if (manager.SelectedViews.get(docView)) {
- manager.SelectedViews.delete(docView);
+ DeselectView(docView?: DocumentView): void {
+ if (docView && manager.SelectedViewsMap.get(docView)) {
+ manager.SelectedViewsMap.delete(docView);
+ manager.SelectedViews.splice(manager.SelectedViews.indexOf(docView), 1);
docView.props.whenChildContentsActiveChanged(false);
}
}
@action
DeselectAll(): void {
manager.SelectedSchemaDocument = undefined;
- Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
- manager.SelectedViews.clear();
+ Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.length = 0;
}
}
const manager = new Manager();
- export function DeselectView(docView: DocumentView): void {
+ export function DeselectView(docView?: DocumentView): void {
manager.DeselectView(docView);
}
- export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
- manager.SelectView(docView, ctrlPressed);
+ export function SelectView(docView: DocumentView | undefined, ctrlPressed: boolean): void {
+ if (!docView) DeselectAll();
+ else manager.SelectView(docView, ctrlPressed);
}
export function SelectSchemaViewDoc(document: Opt<Doc>, deselectAllFirst?: boolean): void {
if (deselectAllFirst) manager.DeselectAll();
@@ -63,7 +74,7 @@ export namespace SelectionManager {
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;
+ return manager.SelectedViewsMap.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
@@ -72,7 +83,7 @@ export namespace SelectionManager {
return !doc
? false
: outsideReaction
- ? manager.SelectedViews.get(doc)
+ ? manager.SelectedViewsMap.get(doc)
? true
: false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
: IsSelectedCache(doc);
@@ -81,7 +92,7 @@ export namespace SelectionManager {
export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
- for (const view of Array.from(manager.SelectedViews.keys())) {
+ for (const view of Array.from(manager.SelectedViewsMap.keys())) {
if (view.props.Document === except) found = view;
}
}
@@ -91,19 +102,22 @@ export namespace SelectionManager {
}
export function Views(): Array<DocumentView> {
- return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews;
+ // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._type_collection !== CollectionViewType.Docking);
}
export function SelectedSchemaDoc(): Doc | undefined {
return manager.SelectedSchemaDocument;
}
export function Docs(): Doc[] {
- return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
+ // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
}
}
-ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
- if (colType === ('tab' as any)) {
+ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
+ if (Doc.noviceMode && expertMode) return false;
+ if (type === 'tab') {
return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
}
- let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
- return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
+ let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+ return selected?.type === type || selected?.type_collection === type || !type;
});
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 2d598c1ac..76037a7e9 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,6 +1,6 @@
-import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr";
-import { Field } from "../../fields/Doc";
-import { ClientUtils } from "./ClientUtils";
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr';
+import { Field } from '../../fields/Doc';
+import { ClientUtils } from './ClientUtils';
let serializing = 0;
export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
@@ -25,7 +25,7 @@ export namespace SerializationHelper {
serializing++;
if (!(obj.constructor.name in reverseMap)) {
serializing--;
- throw Error("Error: " + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ throw Error('Error: ' + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
}
const json = serialize(obj);
@@ -44,12 +44,8 @@ export namespace SerializationHelper {
}
if (!obj.__type) {
- if (true || ClientUtils.RELEASE) {
- console.warn("No property 'type' found in JSON.");
- return undefined;
- } else {
- throw Error("No property 'type' found in JSON.");
- }
+ console.warn("No property 'type' found in JSON.");
+ return undefined;
}
if (!(obj.__type in serializationTypes)) {
@@ -58,87 +54,34 @@ export namespace SerializationHelper {
const type = serializationTypes[obj.__type];
const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));
- if (type.afterDeserialize) {
- await type.afterDeserialize(value);
- }
+ type.afterDeserialize?.(value);
+
return value;
}
}
-const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
+const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
const reverseMap: { [ctor: string]: string } = {};
-export interface DeserializableOpts {
- (constructor: { new(...args: any[]): any }): void;
- withFields(fields: string[]): Function;
-}
-
-export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts;
-export function Deserializable(constructor: { new(...args: any[]): any }): void;
-export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
- function addToMap(name: string, ctor: { new(...args: any[]): any }) {
+export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise<any>, constructorArgs?: [string]): (constructor: { new (...args: any[]): any }) => void {
+ function addToMap(className: string, ctor: { new (...args: any[]): any }) {
const schema = getDefaultModelSchema(ctor) as any;
- if (schema.targetClass !== ctor) {
- const newSchema = { ...schema, factory: () => new ctor() };
- setDefaultModelSchema(ctor, newSchema);
+ if (schema.targetClass !== ctor || constructorArgs) {
+ setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) });
}
- if (!(name in serializationTypes)) {
- serializationTypes[name] = { ctor, afterDeserialize };
- reverseMap[ctor.name] = name;
+ if (!(className in serializationTypes)) {
+ serializationTypes[className] = { ctor, afterDeserialize };
+ reverseMap[ctor.name] = className;
} else {
- throw new Error(`Name ${name} has already been registered as deserializable`);
+ throw new Error(`Name ${className} has already been registered as deserializable`);
}
}
- if (typeof constructor === "string") {
- return Object.assign((ctor: { new(...args: any[]): any }) => {
- addToMap(constructor, ctor);
- }, { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) });
- }
- addToMap(constructor.name, constructor);
-}
-
-export namespace Deserializable {
- export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) {
- return function (constructor: { new(...fields: any[]): any }) {
- Deserializable(name || constructor.name, afterDeserialize)(constructor);
- let schema = getDefaultModelSchema(constructor);
- if (schema) {
- schema.factory = context => {
- const args = fields.map(key => context.json[key]);
- return new constructor(...args);
- };
- // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
- // fields.forEach(field => {
- // if (field in schema.props) {
- // let propSchema = schema.props[field];
- // if (propSchema === false) {
- // return;
- // } else if (propSchema === true) {
- // propSchema = primitive();
- // }
- // schema.props[field] = custom(propSchema.serializer,
- // () => {
- // return SKIP;
- // });
- // }
- // });
- } else {
- schema = {
- props: {},
- factory: context => {
- const args = fields.map(key => context.json[key]);
- return new constructor(...args);
- }
- };
- setDefaultModelSchema(constructor, schema);
- }
- };
- }
+ return (ctor: { new (...args: any[]): any }) => addToMap(className, ctor);
}
export function autoObject(): PropSchema {
return custom(
- (s) => SerializationHelper.Serialize(s),
+ s => SerializationHelper.Serialize(s),
(json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res))
);
-} \ No newline at end of file
+}
diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx
new file mode 100644
index 000000000..f84ad8598
--- /dev/null
+++ b/src/client/util/ServerStats.tsx
@@ -0,0 +1,54 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { MainViewModal } from '../views/MainViewModal';
+import './SharingManager.scss';
+
+@observer
+export class ServerStats extends React.Component<{}> {
+ public static Instance: ServerStats;
+ @observable private isOpen = false; // whether the SharingManager modal is open or not
+
+ // private get linkVisible() {
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
+ // }
+
+ @action
+ public open = async () => {
+ /**
+ * Populates the list of users.
+ */
+ fetch('/stats').then((res: Response) => res.text().then(action(stats => (this._stats = JSON.parse(stats)))));
+
+ this.isOpen = true;
+ };
+
+ public close = action(() => {
+ this.isOpen = false;
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ServerStats.Instance = this;
+ }
+
+ @observable _stats: { [key: string]: any } | undefined;
+
+ /**
+ * @returns the main interface of the SharingManager.
+ */
+ @computed get sharingInterface() {
+ return (
+ <div>
+ <span>Active users:{this._stats?.socketMap.length}</span>
+ {this._stats?.socketMap.map((user: any) => (
+ <p>{user.username}</p>
+ ))}
+ </div>
+ );
+ }
+
+ render() {
+ return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+ }
+}
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index b7199f433..1289ca2b4 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,4 +1,4 @@
-@import "../views/global/globalCssVariables";
+@import '../views/global/globalCssVariables';
.settings-interface {
//background-color: whitesmoke !important;
@@ -59,7 +59,6 @@
}
}
-
.password-content {
display: flex;
flex-direction: column;
@@ -76,7 +75,6 @@
color: black;
border-radius: 5px;
padding: 7px;
-
}
}
@@ -148,7 +146,6 @@
margin-top: 2;
text-align: left;
}
-
}
}
@@ -297,9 +294,8 @@
margin-bottom: 10px;
}
-
.error-text {
- color: #C40233;
+ color: #c40233;
width: 300;
margin-left: -20;
font-size: 10;
@@ -313,7 +309,7 @@
font-size: 10;
margin-bottom: 4;
margin-top: -3;
- color: #009F6B;
+ color: #009f6b;
}
.focus-span {
@@ -352,7 +348,6 @@
padding: 0 0 0 20px;
color: black;
}
-
}
}
@@ -421,7 +416,6 @@
.tab-content {
display: flex;
- margin: 20px 0;
.tab-column {
flex: 0 0 50%;
@@ -437,9 +431,7 @@
.tab-column-content {
padding-left: 16px;
}
-
}
-
}
.tab-column button {
@@ -465,4 +457,4 @@
.settings-interface .settings-heading {
font-size: 25;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 5c1c836f7..f886ce2ca 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -28,7 +28,7 @@ export enum ColorScheme {
export enum freeformScrollMode {
Pan = 'pan',
- Zoom = 'zoom'
+ Zoom = 'zoom',
}
@observer
@@ -74,7 +74,7 @@ export class SettingsManager extends React.Component<{}> {
};
@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 changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_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)));
@@ -176,7 +176,7 @@ export class SettingsManager extends React.Component<{}> {
return (
<div className="prefs-content">
<div>
- <input type="checkbox" onChange={e => (Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().showTitle !== undefined} />
+ <input type="checkbox" onChange={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} checked={Doc.UserDoc().layout_showTitle !== undefined} />
<div className="preferences-check">Show doc header</div>
</div>
<div>
@@ -184,16 +184,20 @@ export class SettingsManager extends React.Component<{}> {
<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>
- <input type="checkbox" onChange={e => FontIconBox.SetRecognizeGesturs(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} />
- <div className="preferences-check">Recognize ink Gesturs</div>
+ <input type="checkbox" onChange={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} />
+ <div className="preferences-check">Recognize ink Gestures</div>
+ </div>
+ <div>
+ <input type="checkbox" onChange={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} checked={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} />
+ <div className="preferences-check">Hide Labels In Ink Shapes</div>
+ </div>
+ <div>
+ <input type="checkbox" onChange={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} checked={BoolCast(Doc.UserDoc().openInkInLightbox)} />
+ <div className="preferences-check">Open Ink Docs in Lightbox</div>
</div>
</div>
);
@@ -307,11 +311,9 @@ export class SettingsManager extends React.Component<{}> {
);
}
-
-
setFreeformScrollMode = (mode: freeformScrollMode) => {
Doc.UserDoc().freeformScrollMode = mode;
- }
+ };
@computed get modesContent() {
return (
@@ -334,12 +336,20 @@ export class SettingsManager extends React.Component<{}> {
<div className="playground-text">Playground Mode</div>
</div>
</div>
- <div className="tab-column-title">Freeform scroll mode</div>
- <div>
- <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>Scroll to pan</button>
- <div>Scrolling pans around the freeform, holding shift and scrolling zooms in and out.</div>
- <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>Scroll to zoom</button>
- <div>Scrolling zooms in and out of canvas</div>
+ <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}>
+ Freeform scrolling
+ </div>
+ <div className="tab-column-content">
+ <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>
+ Scroll to pan
+ </button>
+ <div>
+ <div>Scrolling pans canvas, shift + scrolling zooms</div>
+ </div>
+ <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>
+ Scroll to zoom
+ </button>
+ <div>Scrolling zooms canvas</div>
</div>
</div>
<div className="tab-column">
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 4b0310e76..97e64ab71 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,13 +5,14 @@ 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 { Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc';
+import { AclAdmin, AclPrivate, DocAcl, AclUnset, DocData } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { 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';
@@ -22,8 +23,6 @@ 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;
@@ -82,16 +81,6 @@ export class SharingManager extends React.Component<{}> {
@observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
@observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
- // maps acl symbols to SharingPermissions
- private AclMap = new Map<symbol, string>([
- [AclPrivate, SharingPermissions.None],
- [AclReadonly, SharingPermissions.View],
- [AclAugment, SharingPermissions.Augment],
- [AclSelfEdit, SharingPermissions.SelfEdit],
- [AclEdit, SharingPermissions.Edit],
- [AclAdmin, SharingPermissions.Admin],
- ]);
-
// private get linkVisible() {
// return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
// }
@@ -140,35 +129,21 @@ export class SharingManager extends React.Component<{}> {
if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') {
this.populating = true;
const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
- const raw = JSON.parse(userList) as User[];
- const sharingDocs: ValidatedUser[] = [];
- const evaluating = raw.map(async user => {
- const isCandidate = user.email !== Doc.CurrentUserEmail;
- if (isCandidate) {
- const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
- const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+ const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
+ const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
+ raw.map(
+ action((newUser: User) => {
+ const sharingDoc = docs[newUser.sharingDocumentId];
+ const linkDatabase = docs[newUser.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
- const a1 = await Cast(link?.anchor1, Doc, null);
- const a2 = await Cast(link?.anchor2, Doc, null);
- });
- sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
- }
- }
- });
- return Promise.all(evaluating).then(() => {
- runInAction(async () => {
- for (const sharer of sharingDocs) {
- if (!this.users.find(user => user.user.email === sharer.user.email)) {
- this.users.push(sharer);
- LinkManager.addLinkDB(sharer.linkDatabase);
+ if (!this.users.find(user => user.user.email === newUser.email)) {
+ this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
+ // LinkManager.addLinkDB(sharer.linkDatabase);
}
}
- });
- this.populating = false;
- });
+ })
+ );
+ this.populating = false;
}
};
@@ -184,6 +159,7 @@ export class SharingManager extends React.Component<{}> {
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
return !docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -197,7 +173,7 @@ export class SharingManager extends React.Component<{}> {
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);
+ return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
})
.some(success => !success);
};
@@ -217,6 +193,7 @@ export class SharingManager extends React.Component<{}> {
// ! ensures it returns true if document has been shared successfully, false otherwise
return !docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -239,7 +216,7 @@ export class SharingManager extends React.Component<{}> {
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
+ else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
})
.some(success => !success);
}
@@ -283,6 +260,7 @@ export class SharingManager extends React.Component<{}> {
docs.forEach(doc => {
const isDashboard = dashboards.indexOf(doc) !== -1;
if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
});
}
};
@@ -290,14 +268,18 @@ export class SharingManager extends React.Component<{}> {
/**
* Sets the background of the Dashboard if it has been shared as a visual indicator
*/
- setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
+ setDashboardBackground = (doc: Doc, permission: SharingPermissions) => {
if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
if (permission !== SharingPermissions.None) {
doc.isShared = true;
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])))) {
+ const acls = doc[DocData][DocAcl];
+ if (
+ Object.keys(acls)
+ .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me')
+ .every(key => [AclUnset, AclPrivate].includes(acls[key]))
+ ) {
doc.isShared = undefined;
doc.backgroundColor = undefined;
}
@@ -372,7 +354,7 @@ 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');
+ if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
return dropdownValues
.filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
.map(permission => (
@@ -387,12 +369,11 @@ export class SharingManager extends React.Component<{}> {
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]);
+ if (this.targetDoc && this.targetDocView && docs.length === 1) {
+ DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
}
}}
onPointerEnter={action(() => {
@@ -458,17 +439,6 @@ export class SharingManager extends React.Component<{}> {
}
};
- // distributeOverCollection = (targetDoc?: Doc) => {
- // const target = targetDoc || this.targetDoc!;
-
- // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- // docs.forEach(doc => {
- // for (const [key, value] of Object.entries(doc[AclSym])) {
- // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
- // }
- // });
- // }
-
/**
* Sorting algorithm to sort users.
*/
@@ -491,6 +461,7 @@ export class SharingManager extends React.Component<{}> {
* @returns the main interface of the SharingManager.
*/
@computed get sharingInterface() {
+ if (!this.targetDoc) return null;
TraceMobx();
const groupList = GroupManager.Instance?.allGroups || [];
const sortedUsers = this.users
@@ -515,7 +486,7 @@ export class SharingManager extends React.Component<{}> {
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?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData]));
if (this.myDocAcls) {
const newDocs: Doc[] = [];
@@ -523,21 +494,21 @@ export class SharingManager extends React.Component<{}> {
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
- const targetDoc = docs[0];
+ const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
// tslint:disable-next-line: no-unnecessary-callback-wrapper
const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
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 : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl])));
// 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 uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
return !permissions ? null : (
@@ -563,7 +534,7 @@ export class SharingManager extends React.Component<{}> {
userListContents.unshift(
sameAuthor ? (
<div key={'owner'} className={'container'}>
- <span className={'padding'}>{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : targetDoc?.author}</span>
+ <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span>
<div className="edit-actions">
<div className={'permissions-dropdown'}>Owner</div>
</div>
@@ -573,7 +544,7 @@ export class SharingManager extends React.Component<{}> {
<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 className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div>
</div>
</div>
) : null
@@ -584,7 +555,9 @@ export class SharingManager extends React.Component<{}> {
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 uniform = docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
+ .every(doc => (this.layoutDocAcls ? doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey] : doc?.[DocData]?.[DocAcl]?.[groupKey] === docs[0]?.[DocData]?.[DocAcl]?.[groupKey]));
const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
return !permissions ? null : (
@@ -607,9 +580,8 @@ export class SharingManager extends React.Component<{}> {
</div>
);
});
-
return (
- <div className={'sharing-interface'}>
+ <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'}>
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 2c0a1da8b..ed9819fc0 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -40,9 +40,9 @@ export namespace SnappingManager {
}
export function SetShowSnapLines(show: boolean) {
- runInAction(() => (Doc.UserDoc().showSnapLines = show));
+ runInAction(() => (Doc.UserDoc().freeform_snapLines = show));
}
export function GetShowSnapLines() {
- return Doc.UserDoc().showSnapLines;
+ return Doc.UserDoc().freeform_snapLines;
}
}
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
index 4a2ccd706..0e56ee1bc 100644
--- a/src/client/util/TrackMovements.ts
+++ b/src/client/util/TrackMovements.ts
@@ -3,13 +3,14 @@ import { NumCast } from '../../fields/Types';
import { Doc, DocListCast } from '../../fields/Doc';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { Id } from '../../fields/FieldSymbols';
+import { CollectionViewType } from '../documents/DocumentTypes';
export type Movement = {
time: number;
panX: number;
panY: number;
scale: number;
- docId: string;
+ doc: Doc;
};
export type Presentation = {
@@ -28,7 +29,7 @@ export class TrackMovements {
private tracking: boolean;
private absoluteStart: number;
// instance variable for holding the FFViews and their disposers
- private recordingFFViews: Map<string, IReactionDisposer> | null;
+ private recordingFFViews: Map<Doc, IReactionDisposer> | null;
private tabChangeDisposeFunc: IReactionDisposer | null;
// create static instance and getter for global use
@@ -55,33 +56,33 @@ export class TrackMovements {
return this.currentPresentation.movements === null;
}
- private addRecordingFFView(doc: Doc, key: string = doc[Id]): void {
+ private addRecordingFFView(doc: Doc): 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');
+ if (this.recordingFFViews.has(doc)) {
+ console.warn('addFFView : doc 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)
+ () => ({ x: NumCast(doc.freeform_panX, -1), y: NumCast(doc.freeform_panY, -1), scale: NumCast(doc.freeform_scale, 0) }),
+ res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, doc, res.scale)
);
- this.recordingFFViews?.set(key, disposeFunc);
+ this.recordingFFViews?.set(doc, disposeFunc);
}
- private removeRecordingFFView = (key: string) => {
+ private removeRecordingFFView = (doc: Doc) => {
// 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);
+ this.recordingFFViews.get(doc)?.();
+ this.recordingFFViews.delete(doc);
};
// in the case where only one tab was changed (updates not across dashboards), set only one to true
@@ -89,16 +90,16 @@ export class TrackMovements {
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>();
+ const isFFView = (doc: Doc) => doc && doc._type_collection === CollectionViewType.Freeform;
+ const tabbedFFViews = new Set<Doc>();
for (const DashDoc of tabbedDocs) {
- if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]);
+ if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc);
}
// 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 (!this.recordingFFViews.has(DashDoc)) {
if (isFFView(DashDoc)) {
this.addRecordingFFView(DashDoc);
@@ -110,9 +111,9 @@ export class TrackMovements {
}
// 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);
+ for (const [doc] of this.recordingFFViews) {
+ if (!tabbedFFViews.has(doc)) {
+ this.removeRecordingFFView(doc);
if (onlyOne) return;
}
}
@@ -214,7 +215,7 @@ export class TrackMovements {
}
};
- private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => {
+ private trackMovement = (panX: number, panY: number, doc: Doc, scale: number = 0) => {
// ensure we are recording to track
if (!this.tracking) {
console.error('[recordingApi.ts] trackMovements(): tracking is false');
@@ -231,10 +232,10 @@ export class TrackMovements {
// get the time
const time = new Date().getTime() - this.absoluteStart;
// make new movement object
- const movement: Movement = { time, panX, panY, scale, docId };
+ const movement: Movement = { time, panX, panY, scale, doc };
// add that movement to the current presentation data's movement array
- this.currentPresentation.movements && this.currentPresentation.movements.push(movement);
+ this.currentPresentation.movements?.push(movement);
};
// method that concatenates an array of presentatations into one
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d0aec45a6..b59af6656 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,9 +1,10 @@
import { observable, action, runInAction } from 'mobx';
+import { Field } from '../../fields/Doc';
import { Without } from '../../Utils';
function getBatchName(target: any, key: string | symbol): string {
const keyName = key.toString();
- if (target && target.constructor && target.constructor.name) {
+ if (target?.constructor?.name) {
return `${target.constructor.name}.${keyName}`;
}
return keyName;
@@ -34,6 +35,17 @@ function propertyDecorator(target: any, key: string | symbol) {
});
}
+export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any {
+ return function () {
+ const batch = UndoManager.StartBatch(batchName);
+ try {
+ return fn.apply(undefined, arguments as any);
+ } finally {
+ batch.end();
+ }
+ };
+}
+
export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any;
export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
@@ -73,15 +85,18 @@ export namespace UndoManager {
}
type UndoBatch = UndoEvent[];
+ export let undoStackNames: string[] = observable([]);
+ export let redoStackNames: string[] = observable([]);
export let undoStack: UndoBatch[] = observable([]);
export let redoStack: UndoBatch[] = observable([]);
let currentBatch: UndoBatch | undefined;
- export let batchCounter = 0;
+ export let batchCounter = observable.box(0);
let undoing = false;
- let tempEvents: UndoEvent[] | undefined = undefined;
+ export let tempEvents: UndoEvent[] | undefined = undefined;
- export function AddEvent(event: UndoEvent): void {
- if (currentBatch && batchCounter && !undoing) {
+ export function AddEvent(event: UndoEvent, value?: any): void {
+ if (currentBatch && batchCounter.get() && !undoing) {
+ console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + (value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)));
currentBatch.push(event);
tempEvents?.push(event);
}
@@ -135,11 +150,13 @@ export namespace UndoManager {
private dispose = (cancel: boolean) => {
if (this.disposed) {
- throw new Error('Cannot dispose an already disposed batch');
+ console.log('WARNING: undo batch already disposed');
+ return false;
+ } else {
+ this.disposed = true;
+ openBatches.splice(openBatches.indexOf(this));
+ return EndBatch(this.batchName, cancel);
}
- this.disposed = true;
- openBatches.splice(openBatches.indexOf(this));
- return EndBatch(cancel);
};
end = () => this.dispose(false);
@@ -147,22 +164,23 @@ export namespace UndoManager {
}
export function StartBatch(batchName: string): Batch {
- // console.log("Start " + batchCounter + " " + batchName);
- batchCounter++;
- if (batchCounter > 0 && currentBatch === undefined) {
+ console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+ runInAction(() => batchCounter.set(batchCounter.get() + 1));
+ if (currentBatch === undefined) {
currentBatch = [];
}
return new Batch(batchName);
}
- const EndBatch = action((cancel: boolean = false) => {
- batchCounter--;
- // console.log("End " + batchCounter);
- if (batchCounter === 0 && currentBatch?.length) {
- // console.log("------ended----")
+ const EndBatch = action((batchName: string, cancel: boolean = false) => {
+ runInAction(() => batchCounter.set(batchCounter.get() - 1));
+ console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+ if (batchCounter.get() === 0 && currentBatch?.length) {
if (!cancel) {
undoStack.push(currentBatch);
+ undoStackNames.push(batchName ?? '???');
}
+ redoStackNames.length = 0;
redoStack.length = 0;
currentBatch = undefined;
return true;
@@ -170,15 +188,11 @@ export namespace UndoManager {
return false;
});
- export function RunInTempBatch<T>(fn: () => T) {
+ export function StartTempBatch() {
tempEvents = [];
- try {
- const success = runInAction(fn);
- if (!success) UndoManager.UndoTempBatch();
- return success;
- } finally {
- tempEvents = undefined;
- }
+ }
+ export function EndTempBatch<T>(success: boolean) {
+ UndoManager.UndoTempBatch(success);
}
//TODO Make this return the return value
export function RunInBatch<T>(fn: () => T, batchName: string) {
@@ -189,10 +203,11 @@ export namespace UndoManager {
batch.end();
}
}
- export const UndoTempBatch = action(() => {
- if (tempEvents) {
+ export const UndoTempBatch = action((success: any) => {
+ if (tempEvents && !success) {
undoing = true;
for (let i = tempEvents.length - 1; i >= 0; i--) {
+ currentBatch?.includes(tempEvents[i]) && currentBatch.splice(currentBatch.indexOf(tempEvents[i]));
tempEvents[i].undo();
}
undoing = false;
@@ -204,6 +219,7 @@ export namespace UndoManager {
return;
}
+ const names = undoStackNames.pop();
const commands = undoStack.pop();
if (!commands) {
return;
@@ -215,6 +231,7 @@ export namespace UndoManager {
}
undoing = false;
+ redoStackNames.push(names ?? '???');
redoStack.push(commands);
});
@@ -223,6 +240,7 @@ export namespace UndoManager {
return;
}
+ const names = redoStackNames.pop();
const commands = redoStack.pop();
if (!commands) {
return;
@@ -234,6 +252,7 @@ export namespace UndoManager {
}
undoing = false;
+ undoStackNames.push(names ?? '???');
undoStack.push(commands);
});
}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.ts
index 502e0fbac..57e8516ac 100644
--- a/src/client/util/request-image-size.js
+++ b/src/client/util/request-image-size.ts
@@ -13,7 +13,7 @@ const request = require('request');
const imageSize = require('image-size');
const HttpError = require('standard-http-error');
-module.exports = function requestImageSize(options) {
+module.exports = function requestImageSize(options: any) {
let opts = {
encoding: null,
};
@@ -36,28 +36,24 @@ module.exports = function requestImageSize(options) {
return new Promise((resolve, reject) => {
const req = request(opts);
- req.on('response', res => {
+ req.on('response', (res: any) => {
if (res.statusCode >= 400) {
return reject(new HttpError(res.statusCode, res.statusMessage));
}
let buffer = Buffer.from([]);
- let size;
+ let size: any;
- res.on('data', chunk => {
+ res.on('data', (chunk: any) => {
buffer = Buffer.concat([buffer, chunk]);
try {
size = imageSize(buffer);
- } catch (err) {
- reject(err);
- return req.abort();
- }
-
- if (size) {
- resolve(size);
- return req.abort();
- }
+ if (size) {
+ resolve(size);
+ return req.abort();
+ }
+ } catch (err) {}
});
res.on('error', reject);