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.ts330
-rw-r--r--src/client/util/DictationManager.ts14
-rw-r--r--src/client/util/DocumentManager.ts379
-rw-r--r--src/client/util/DragManager.ts121
-rw-r--r--src/client/util/DropConverter.ts72
-rw-r--r--src/client/util/HypothesisUtils.ts160
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx334
-rw-r--r--src/client/util/InteractionUtils.tsx286
-rw-r--r--src/client/util/LinkFollower.ts162
-rw-r--r--src/client/util/LinkManager.ts157
-rw-r--r--src/client/util/ReplayMovements.ts104
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx297
-rw-r--r--src/client/util/Scripting.ts16
-rw-r--r--src/client/util/SelectionManager.ts57
-rw-r--r--src/client/util/SerializationHelper.ts97
-rw-r--r--src/client/util/SettingsManager.scss16
-rw-r--r--src/client/util/SettingsManager.tsx42
-rw-r--r--src/client/util/SharingManager.scss3
-rw-r--r--src/client/util/SharingManager.tsx97
-rw-r--r--src/client/util/request-image-size.ts (renamed from src/client/util/request-image-size.js)44
-rw-r--r--src/client/util/type_decls.d3
23 files changed, 1686 insertions, 1232 deletions
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 0b5957fac..c9fcc84a3 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.anchor1)?.title)}
+ </div>
+ )
+ );
}
return (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d19874720..9aceed366 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,15 +1,17 @@
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 { ScriptField } from "../../fields/ScriptField";
-import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
+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";
import { OmitKeys, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
@@ -19,6 +21,7 @@ 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 { OpenWhere } from "../views/nodes/DocumentView";
import { OverlayView } from "../views/OverlayView";
import { DragManager } from "./DragManager";
import { MakeTemplate } from "./DropConverter";
@@ -38,12 +41,16 @@ interface Button {
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;
// fields that do not correspond to DocumentOption fields
- scripts?: { script?: string; onClick?: string; }
+ scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
funcs?: { [key:string]: string };
subMenu?: Button[];
}
@@ -56,7 +63,7 @@ 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, system: true },
template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
@@ -76,7 +83,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);
@@ -89,7 +96,7 @@ 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,
+ _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
@@ -103,7 +110,7 @@ export class CurrentUserUtils {
const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
{ opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"},
- { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}];
+ { 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;
@@ -119,11 +126,11 @@ export class CurrentUserUtils {
const tempClicks = DocCast(doc[field]);
const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
- { opts: { title: "onClick"}, script: "console.log( 'click')"},
- { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"},
- { opts: { title: "onChildClick"}, script: "console.log( 'child click')"},
- { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"},
- { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"},
+ { 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};
@@ -143,7 +150,7 @@ export class CurrentUserUtils {
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", width:200, system: true};
+ const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, 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");
});
@@ -154,7 +161,7 @@ export class CurrentUserUtils {
/// Initializes collection of templates for notes and click functions
static setupDocTemplates(doc: Doc, field="myTemplates") {
- DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"});
+ DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(), { });
const templates = [
CurrentUserUtils.setupNoteTemplates(doc),
CurrentUserUtils.setupClickEditorTemplates(doc)
@@ -178,10 +185,10 @@ export class CurrentUserUtils {
case DocumentType.IMG : creator = imageBox; break;
case DocumentType.FONTICON: creator = fontBox; break;
}
- const allopts = {system: true, ...opts};
+ const allopts = {system: 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
@@ -196,6 +203,7 @@ export class CurrentUserUtils {
makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}),
makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
+ makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}),
//nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
@@ -205,12 +213,12 @@ export class CurrentUserUtils {
DocUtils.AssignOpts(DocCast(doc[field]), {}, iconTemplates);
}
- /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation
+ /// 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, _fitWidth: false, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
const json = {
doc: {
type: "doc",
@@ -218,11 +226,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: "creationDate", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}]
}]
@@ -235,9 +243,9 @@ export class CurrentUserUtils {
const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text",
layout:
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
- ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
- " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
+ ` <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>"
}, "header");
@@ -254,21 +262,21 @@ export class CurrentUserUtils {
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: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _fitWidth: true}},
+ {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _fitWidth: true }},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _backgroundGridShow: true, }},
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
{key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
- {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }},
+ {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _fitWidth: true, _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: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
// {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
- {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
- {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
- {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
+ {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, treeViewHideUnrendered: true}},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _fitWidth: true, _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,
@@ -279,30 +287,35 @@ export class CurrentUserUtils {
emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs));
return [
- { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, },
- { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, },
- { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, },
- { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, },
- { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, },
- { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, },
- { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, },
- { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
- { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: {onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
- ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, }))
+ { 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 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 an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)},
+ { 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: "repl", icon: "calculator", clickFactory: 'repl' 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",
+ const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
+ _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true,
btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
_removeDropProperties: new List<string>(["_stayInCollection"]),
};
@@ -312,7 +325,7 @@ export class CurrentUserUtils {
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
+ childDropAction: 'alias'
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -320,16 +333,17 @@ export class CurrentUserUtils {
/// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents
static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] {
- const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
+ const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
+ const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails";
return [
- { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", },
+ { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} },
{ title: "Search", target: this.setupSearcher(doc, "mySearcher"), icon: "search", },
{ title: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", },
{ title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} },
{ title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", },
{ title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", },
{ title: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}},
- { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", },
+ { title: "Trails", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}},
{ title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}}));
}
@@ -347,15 +361,15 @@ export class CurrentUserUtils {
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"]),
+ _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
+ _removeDropProperties: new List<string>(["_stayInCollection"]),
};
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", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
+ _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
}
@@ -392,14 +406,14 @@ export class CurrentUserUtils {
// 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,
+ _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
borderRounding: "5px", boxShadow: "0 0", system: true
}) as any as Doc
// sets up the text container for the information contained within the mobile button
static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
...opts,
- _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
+ _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true
}) as any as Doc
@@ -457,28 +471,33 @@ export class CurrentUserUtils {
static setupDashboards(doc: Doc, field:string) {
var myDashboards = DocCast(doc[field]);
+ 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 reqdBtnScript = {onClick: newDashboard,}
const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
+ const contextMenuScripts = [/*newDashboard*/] as string[];
+ const contextMenuLabels = [/*"Create New Dashboard"*/] as string[];
+ const contextMenuIcons = [/*"plus"*/] as string[];
+ const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)', 'resetDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
+ const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
+ 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: 150, ignoreClick: 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,
- contextMenuLabels: new List<string>(["Create New Dashboard"]),
- contextMenuIcons: new List<string>(["plus"]),
- childContextMenuLabels: new List<string>(["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]),// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
- childContextMenuIcons: new List<string>(["chalkboard", "tv", "camera", "users", "times"]), // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
+ 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."
};
myDashboards = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
- const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`;
- const contextMenuScripts = [newDashboard];
- const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
- const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
myDashboards.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
}
@@ -491,31 +510,6 @@ export class CurrentUserUtils {
return myDashboards;
}
- /// initializes the left sidebar Trails pane
- static setupTrails(doc: Doc, field:string) {
- var myTrails = DocCast(doc[field]);
- const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true,
- title: "New trail", toolTip: "Create new trail", btnType: ButtonType.ClickButton, buttonText: "New trail", icon: "plus", system: true };
- const reqdBtnScript = {onClick: `createNewPresentation()`};
- const newTrailButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myTrails?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
-
- const reqdOpts:DocumentOptions = {
- title: "My Trails", _showTitle: "title", _height: 100,
- treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
- contextMenuIcons: new List<string>(["plus"]),
- contextMenuLabels: new List<string>(["Create New Trail"]),
- _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
- explainer: "All of the trails that you have created will appear here."
- };
- myTrails = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
- const contextMenuScripts = [reqdBtnScript.onClick];
- if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
- myTrails.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
- }
- return myTrails;
- }
-
/// initializes the left sidebar File system pane
static setupFilesystem(doc: Doc, field:string) {
var myFilesystem = DocCast(doc[field]);
@@ -532,7 +526,7 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true,
isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true,
- treeViewTruncateTitleWidth: 150, ignoreClick: true, childDropAction: "alias",
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "alias",
childContextMenuLabels: new List<string>(["Create new folder"]),
childContextMenuIcons: new List<string>(["plus"]),
explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
@@ -549,7 +543,7 @@ export class CurrentUserUtils {
static setupRecentlyClosed(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true,
- treeViewTruncateTitleWidth: 150, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
contextMenuLabels: new List<string>(["Empty recently closed"]),
contextMenuIcons:new List<string>(["trash"]),
explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -575,7 +569,7 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = {
_lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true,
- treeViewHideTitle: true, treeViewTruncateTitleWidth: 150
+ treeViewHideTitle: true, treeViewTruncateTitleWidth: 350
};
if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]);
@@ -588,25 +582,27 @@ export class CurrentUserUtils {
})
static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
- btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true,
- _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]),
+ btnType: ButtonType.ToolButton, _forceActive: true, _hideContextMenu: true,
+ _removeDropProperties: new List<string>([ "_hideContextMenu", "stayInCollection"]),
_nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts,
})
/// initializes the required buttons in the expanding button menu at the bottom of the Dash window
static setupDockedButtons(doc: Doc, field="myDockedBtns") {
const dockedBtns = DocCast(doc[field]);
- const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}) =>
+ const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string|undefined}, funcs?: {[key:string]:string}) =>
DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ??
- CurrentUserUtils.createToolButton(opts), scripts);
+ 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: { 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 media"}},
];
const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
- const dockBtnsReqdOpts = {
- title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true,
+ const dockBtnsReqdOpts:DocumentOptions = {
+ title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'alias',
childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
};
reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
@@ -614,53 +610,68 @@ export class CurrentUserUtils {
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
+ ]
+ }
+ static viewTools(): Button[] {
+ return [
+ { 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: "Snap\xA0Lines",icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snap lines", 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: "Arrange", icon: "window", toolTip: "Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Reset", icon: "check", toolTip: "Reset View", btnType: ButtonType.ClickButton, expertMode: false, backgroundColor:"transparent", scripts: { onClick: 'resetView()'}}, // 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: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
+ { title: "Color", toolTip: "Font 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", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+ { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Right", toolTip: "Right align", 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_);}'}},
- { 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", _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} },
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
- // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
- { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} },
- // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' },
- { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick: '{ return setActiveTool("line", _readOnly_);}' }},
- { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: "{ return setFillColor(value, _readOnly_);}"} },
- { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
- { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
+ { 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: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip", toolType: "fillColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
+ { title: "Color", 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_); }'}, }];
}
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_); }'} },
];
}
@@ -672,16 +683,21 @@ 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: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
- { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
- { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Text", icon: "text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
- { title: "Ink", icon: "ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', inearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`} }, // Always available
- { title: "Web", icon: "web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected
- { title: "Schema", icon: "schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+ 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: "Z order", icon: "z", toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(_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: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearViewIsExpanded: `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)`, linearViewIsExpanded: `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)`, linearViewIsExpanded: `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)`, linearViewIsExpanded: `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)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
];
}
@@ -689,23 +705,24 @@ 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
+ 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, system: true, dontUndo: true,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
- _height: 30, _nativeHeight: 30,
+ _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
+ toolType: params.toolType, expertMode: params.expertMode,
_stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
- _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ _removeDropProperties: new List<string>([ "_stayInCollection"]),
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
- backgroundColor: params.scripts?.onClick /// a bit hacky. if onClick is set, then we assume it returns a color value when queried with '_readOnly_'. This will be true for toggle buttons, but not generally
+ backgroundColor: params.btnType === ButtonType.ToggleButton ? params.scripts?.onClick:undefined /// a bit hacky. if onClick is set, then we assume it returns a color value when queried with '_readOnly_'. This will be true for toggle buttons, but not generally
}
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs);
}
/// 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", flexGap: 0, childDropAction: 'alias', childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
@@ -713,13 +730,13 @@ export class CurrentUserUtils {
return this.setupContextMenuButton(params, menuBtnDoc);
} else {
const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true,
+ childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
linearViewSubMenu: true, linearViewExpandable: true, };
const items = params.subMenu?.map(sub =>
this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
);
return DocUtils.AssignScripts(
- DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs);
+ DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), params.scripts, params.funcs);
}
});
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
@@ -759,12 +776,13 @@ export class CurrentUserUtils {
const sharedDocOpts:DocumentOptions = {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
+ isFolder:true,
childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
childContextMenuLabels: new List<string>(["Add to Dashboards",]),
childContextMenuIcons: new List<string>(["user-plus",]),
"acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
// 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'"
@@ -806,7 +824,10 @@ export class CurrentUserUtils {
doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true);
doc._showLabel ?? (doc._showLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
- doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
+ doc.activeTool = InkTool.None;
+ 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 = "");
@@ -835,6 +856,8 @@ export class CurrentUserUtils {
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
+ 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;
@@ -873,10 +896,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 {
@@ -948,6 +973,7 @@ ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "docu
ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
+ScriptingGlobals.add(function 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..e116ae14b 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,7 @@ 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,16 +324,6 @@ 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) => {
@@ -345,7 +335,7 @@ export namespace DictationManager {
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);
},
},
],
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 52b643c04..ccf370662 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,19 +1,24 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { loadAsync } from 'jszip';
+import { action, observable, ObservableSet, runInAction } from 'mobx';
+import { AnimationSym, Doc, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
-import { DocumentType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
-import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
-import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
+import { listSpec } from '../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
+import { emptyFunction } from '../../Utils';
+import { CollectionViewType } from '../documents/DocumentTypes';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
import { CollectionView } from '../views/collections/CollectionView';
+import { TabDocView } from '../views/collections/TabDocView';
+import { LightboxView } from '../views/LightboxView';
+import { MainView } from '../views/MainView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
+import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
+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 {
@@ -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.getDocumentViewById(doc[Id]);
+ 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 link = view.rootDoc;
+ this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+ this.LinkedDocumentViews.push({
+ a: viewAnchorIndex === 'anchor2' ? otherView : view,
+ b: viewAnchorIndex === 'anchor2' ? view : otherView,
+ l: link,
+ })
+ );
this.LinkAnchorBoxViews.push(view);
// this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " +
// view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString));
} else {
this.DocumentViews.add(view);
}
+ this.callAddViewFuncs(view);
};
public RemoveView = action((view: DocumentView) => {
this.LinkedDocumentViews.slice().forEach(
@@ -122,19 +157,35 @@ export class DocumentManager {
}
public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
- return this.getDocumentViewById(toFind[Id], preferredCollection);
+ const found =
+ // 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;
+ return this.getDocumentViewById(found[Id], preferredCollection);
}
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);
+ 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));
@@ -150,192 +201,132 @@ export class DocumentManager {
return toReturn;
}
+ static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
+ if (!doc) return [];
+ const srcContext = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
+ var containerDocContext = srcContext ? [srcContext, doc] : [doc];
+ while (
+ containerDocContext.length &&
+ containerDocContext[0]?.context &&
+ DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking &&
+ (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0]))
+ ) {
+ containerDocContext = [Cast(containerDocContext[0].context, 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 = async (
+
+ // 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();
+ return rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined }));
+ };
+
+ // 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,
- presZoom?: number
- ): Promise<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 ?? targetDoc : targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
- var wasHidden = resolvedTarget.hidden;
- if (wasHidden) {
- runInAction(() => {
- resolvedTarget.hidden = false; // if the target is hidden, un-hide it here.
- docView?.props.bringToFront(resolvedTarget);
- });
- }
- const focusAndFinish = (didFocus: boolean) => {
- const finalTargetDoc = resolvedTarget;
- if (originatingDoc?.isPushpin) {
- if (!didFocus && !wasHidden) {
- // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
- 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,
- });
- }
- }
- }
- }
- finished?.();
+ 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 = await new Promise<DocumentView>(res => {
+ const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
+ if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
+ options.didMove = true;
+ docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation as OpenWhere);
+ this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
+ });
+
+ docContextPath.shift();
+ const childViewIterator = async (docView: DocumentView) => {
+ const innerDoc = docContextPath.shift();
+ return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
};
- const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
- const contextDocs = docContext.length ? await DocListCastAsync(docContext[0].data) : undefined;
- const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
- const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
- const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (annoContainerView) {
- if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
- annoContainerView.iconify(() =>
- annoContainerView.focus(targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoom,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- })
- );
- return;
- } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
- annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
- }
+ const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
+ this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
+
+ finished?.();
+ };
+
+ 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.layoutKey === '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;
}
- 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: presZoom,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(forceDidFocus || didFocus);
- res(ViewAdjustment.doNothing);
- }),
- });
- if (focusView.props.Document.layoutKey === 'layout_icon') {
- focusView.iconify(() => doFocus(true));
- } else {
- doFocus(false);
- }
- } else {
- if (!targetDocContext) {
- // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
- createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
- } else {
- // otherwise try to get a view of the context of the target
- if (targetDocContextView) {
- // we found a context view and aren't forced to create a new one ... focus on the context first..
- wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
- targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
- targetDocContext._viewTransition = 'transform 500ms';
- targetDocContextView.props.focus(targetDocContextView.rootDoc, {
- willZoom,
- afterFocus: async () => {
- targetDocContext._viewTransition = undefined;
- if (targetDocContext.layoutKey === 'layout_icon') {
- targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom));
- }
- return ViewAdjustment.doNothing;
- },
- });
+ };
- // 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, presZoom)
- );
- }
- }
- // 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
- );
- }
+ @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[AnimationSym] = 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[AnimationSym] = undefined;
+ DocumentManager.removeOverlayViews();
+ contextView && (contextView.htmlOverlayEffect = '');
+ });
}
- };
+ }
}
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);
+ if (dv) {
+ DocumentManager.Instance.showDocumentView(dv, { willZoomCentered: true });
} else {
const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
const showDoc = context || doc;
- const bestAlias = showDoc === Doc.GetProto(showDoc) ? DocListCast(showDoc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : showDoc;
-
- CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc));
+ DocumentManager.Instance.showDocument(Doc.BestAlias(showDoc), { openLocation: OpenWhere.addRight }, () => DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }));
}
}
ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index ccd94c56e..7d2aa813f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
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';
@@ -10,7 +10,9 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types
import { emptyFunction, Utils } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import * as globalCssVariables from '../views/global/globalCssVariables.scss';
+import { Colors } from '../views/global/globalEnums';
import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
@@ -24,7 +26,7 @@ export type dropActionType = 'alias' | 'copy' | 'move' | 'same' | 'proto' | 'non
* @param dropAction: What to do with the document when it is dropped
* @param dragStarted: Method to call when the drag is started
*/
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc | undefined> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
const onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
@@ -149,10 +151,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 +168,6 @@ export namespace DragManager {
this.dropDocCreator = dropDocCreator;
this.offset = [0, 0];
}
- linkSourceDoc?: Doc;
dropDocCreator: (annotationOn: Doc | undefined) => Doc;
dropDocument?: Doc;
offset: number[];
@@ -201,24 +206,26 @@ export namespace DragManager {
dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
- docDragData.droppedDocuments = await Promise.all(
- dragData.draggedDocuments.map(async d =>
- !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
- ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
- : docDragData.dropAction === 'alias'
- ? Doc.MakeAlias(d)
- : docDragData.dropAction === 'proto'
- ? Doc.GetProto(d)
- : docDragData.dropAction === 'copy'
- ? (
- await Doc.MakeClone(d)
- ).clone
- : d
+ 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
+ )
)
- );
+ ).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 dragProps = StrListCast(dragData.draggedDocuments[i].removeDropProperties);
const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps));
remProps.map(prop => (drop[prop] = undefined));
});
@@ -251,17 +258,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 StartImgDrag(ele: HTMLElement, downX: number, downY: number) {
- StartDrag([ele], {}, downX, downY);
+ export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(ele, dragData, downX, downY, options);
}
export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -325,7 +328,7 @@ export namespace DragManager {
export let DocDragData: DocumentDragData | undefined;
export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
if (dragData.dropAction === 'none') return;
- DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
+ DocDragData = dragData as DocumentDragData;
const batch = UndoManager.StartBatch('dragging');
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
@@ -344,8 +347,7 @@ export namespace DragManager {
}
Object.assign(dragDiv.style, { width: '', height: '', overflow: '' });
dragDiv.hidden = false;
- const scaleXs: number[] = [],
- scaleYs: number[] = [],
+ const scalings: number[] = [],
xs: number[] = [],
ys: number[] = [];
@@ -355,8 +357,18 @@ export namespace DragManager {
top: Number.MAX_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
};
+ let rot = 0;
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
+ let useDim = false; // if doc is rotated by freeformview, then the dragged elements width and height won't reflect the unrotated dimensions, so we need to rely on the element knowing its own width/height. \
+ // 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);
+ } else {
+ useDim = true;
+ }
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
const children = Array.from(dragElement.children);
@@ -376,17 +388,28 @@ export namespace DragManager {
}
}
const rect = ele.getBoundingClientRect();
- const scaleX = rect.width / (ele.offsetWidth || rect.width);
- const scaleY = ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
+ const w = ele.offsetWidth || rect.width;
+ const h = ele.offsetHeight || rect.height;
+ const rotR = -((rot < 0 ? rot + 360 : rot) / 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];
+ const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w];
+ const minx = Math.min(tl[0], tr[0], br[0], bl[0]);
+ const maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
+ const miny = Math.min(tl[1], tr[1], br[1], bl[1]);
+ const maxy = Math.max(tl[1], tr[1], br[1], bl[1]);
+ const scaling = rect.width / (Math.abs(maxx - minx) || 1);
elesCont.left = Math.min(rect.left, elesCont.left);
elesCont.top = Math.min(rect.top, elesCont.top);
elesCont.right = Math.max(rect.right, elesCont.right);
elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
- xs.push(rect.left);
- ys.push(rect.top);
- scaleXs.push(scaleX);
- scaleYs.push(scaleY);
+ xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
+ ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
+ scalings.push(scaling);
+ const width = useDim ? getComputedStyle(ele).width : '';
+ const height = useDim ? getComputedStyle(ele).height : '';
Object.assign(dragElement.style, {
opacity: '0.7',
position: 'absolute',
@@ -399,11 +422,11 @@ export namespace DragManager {
borderRadius: getComputedStyle(ele).borderRadius,
zIndex: globalCssVariables.contextMenuZindex,
transformOrigin: '0 0',
- width: `${rect.width / scaleX}px`,
- height: `${rect.height / scaleY}px`,
- transform: `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`,
+ width,
+ height,
+ transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
});
- dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`;
+ dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
if (docsToDrag.length) {
const pdfBox = dragElement.getElementsByTagName('canvas');
@@ -434,7 +457,7 @@ export namespace DragManager {
const hideDragShowOriginalElements = (hide: boolean) => {
dragLabel.style.display = hide ? '' : 'none';
!hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.forEach(ele => (ele.hidden = hide));
+ setTimeout(() => eles.forEach(ele => (ele.hidden = hide)));
};
options?.hideSource && hideDragShowOriginalElements(true);
@@ -457,8 +480,7 @@ export namespace DragManager {
document.removeEventListener('pointerup', upHandler, true);
SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
- const ended = batch.end();
- if (undo && ended) UndoManager.Undo();
+ if (batch.end() && undo) UndoManager.Undo();
docsBeingDragged.length = 0;
});
var startWindowDragTimer: any;
@@ -543,8 +565,8 @@ export namespace DragManager {
const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
lastPt = { x, y };
- dragLabel.style.transform = `translate(${xs[0] + moveVec.x + (options?.offsetX || 0)}px, ${ys[0] + moveVec.y + (options?.offsetY || 0) - 20}px)`;
- dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`));
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`));
+ dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
};
const upHandler = (e: PointerEvent) => {
clearTimeout(startWindowDragTimer);
@@ -575,3 +597,8 @@ export namespace DragManager {
endDrag?.();
}
}
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
+ if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent';
+ DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+});
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 256ab5c44..7c209d1e0 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,37 +1,41 @@
-import { DragManager } from "./DragManager";
-import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { ObjectField } from "../../fields/ObjectField";
-import { StrCast, Cast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
-import { ScriptField, ComputedField } from "../../fields/ScriptField";
-import { RichTextField } from "../../fields/RichTextField";
-import { ImageField } from "../../fields/URLField";
-import { ScriptingGlobals } from "./ScriptingGlobals";
-import { listSpec } from "../../fields/Schema";
+import { DragManager } from './DragManager';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { ObjectField } from '../../fields/ObjectField';
+import { StrCast, Cast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
+import { ScriptField, ComputedField } from '../../fields/ScriptField';
+import { RichTextField } from '../../fields/RichTextField';
+import { ImageField } from '../../fields/URLField';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { listSpec } from '../../fields/Schema';
+import { ButtonType } from '../views/nodes/button/FontIconBox';
-export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") {
+export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') {
if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated
doc.isTemplateDoc = makeTemplate(doc, first, rename);
return doc;
}
-//
+//
// converts 'doc' into a template that can be used to render other documents.
// the title of doc is used to determine which field is being templated, so
-// passing a value for 'rename' allows the doc to be given a meangingful name
+// passing a value for 'rename' allows the doc to be given a meangingful name
// after it has been converted to
function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
- if (layoutDoc.layout instanceof Doc) { // its already a template
+ if (layoutDoc.layout instanceof Doc) {
+ // its already a template
return true;
}
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
- const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
+ const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, '');
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
docs.forEach(d => {
- if (!StrCast(d.title).startsWith("-")) {
- const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "");
+ 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;
@@ -43,12 +47,13 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
}
});
if (first) {
- if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template
+ if (!docs.length) {
+ // bcz: feels hacky : if the root level document has items, it's not a field template
any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any;
}
}
if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
- if (!StrCast(layoutDoc.title).startsWith("-")) {
+ if (!StrCast(layoutDoc.title).startsWith('-')) {
any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
}
}
@@ -59,23 +64,29 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data?.draggedDocuments.map((doc, i) => {
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 (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 dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
- remProps.map(prop => dbox[prop] = undefined);
+ remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
- const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type !== DocumentType.FONTICON) {
!layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
}
layoutDoc.isTemplateDoc = true;
dbox = Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
- backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt"
+ _nativeWidth: 100,
+ _nativeHeight: 100,
+ _width: 100,
+ _height: 100,
+ backgroundColor: StrCast(doc.backgroundColor),
+ title: StrCast(layoutDoc.title),
+ btnType: ButtonType.ClickButton,
+ icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt',
});
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
@@ -86,5 +97,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data.droppedDocuments[i] = dbox;
});
}
-ScriptingGlobals.add(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); },
- "converts the dropped data to buttons", "(dragData: any)"); \ No newline at end of file
+ScriptingGlobals.add(
+ function convertToButtons(dragData: any) {
+ convertDropDataToButtons(dragData as DragManager.DocumentDragData);
+ },
+ 'converts the dropped data to buttons',
+ '(dragData: any)'
+);
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index e910a9118..297520d5d 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -1,30 +1,28 @@
-import { StrCast, Cast } from "../../fields/Types";
-import { SearchUtil } from "./SearchUtil";
-import { action, runInAction } from "mobx";
-import { Doc, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { Docs } from "../documents/Documents";
-import { SelectionManager } from "./SelectionManager";
-import { WebField } from "../../fields/URLField";
-import { DocumentManager } from "./DocumentManager";
-import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton";
-import { simulateMouseClick, Utils } from "../../Utils";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { Id } from "../../fields/FieldSymbols";
+import { StrCast, Cast } from '../../fields/Types';
+import { SearchUtil } from './SearchUtil';
+import { action, runInAction } from 'mobx';
+import { Doc, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
+import { SelectionManager } from './SelectionManager';
+import { WebField } from '../../fields/URLField';
+import { DocumentManager } from './DocumentManager';
+import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton';
+import { simulateMouseClick, Utils } from '../../Utils';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { Id } from '../../fields/FieldSymbols';
export namespace Hypothesis {
-
/**
- * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
+ * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
* If none exist, create and return a new WebDocument.
*/
export const getSourceWebDoc = async (uri: string) => {
const result = await findWebDoc(uri);
- console.log(result ? "existing doc found" : "existing doc NOT found");
+ 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
};
-
/**
* Search for a WebDocument whose url field matches the given uri, return undefined if not found
*/
@@ -33,18 +31,18 @@ export namespace Hypothesis {
if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
const results: Doc[] = [];
- await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => {
- const docs = res.docs;
- const filteredDocs = docs.filter(doc =>
- doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data
- );
- filteredDocs.forEach(doc => {
- uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
- });
- }));
+ await SearchUtil.Search('web', true).then(
+ action(async (res: SearchUtil.DocSearchResult) => {
+ const docs = res.docs;
+ const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data);
+ filteredDocs.forEach(doc => {
+ uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
+ });
+ })
+ );
const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
- return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen
+ return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen
};
/**
@@ -52,17 +50,19 @@ export namespace Hypothesis {
*/
export const linkListener = async (e: any) => {
const annotationId: string = e.detail.id;
- const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation
+ const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation
const sourceDoc: Doc = await getSourceWebDoc(annotationUri);
- if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
+ if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) {
+ // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
runInAction(() => {
DocumentLinksButton.AnnotationId = annotationId;
DocumentLinksButton.AnnotationUri = annotationUri;
DocumentLinksButton.StartLink = sourceDoc;
DocumentLinksButton.StartLinkView = undefined;
});
- } else { // if a link has already been started, complete the link to sourceDoc
+ } else {
+ // if a link has already been started, complete the link to sourceDoc
runInAction(() => {
DocumentLinksButton.AnnotationId = annotationId;
DocumentLinksButton.AnnotationUri = annotationUri;
@@ -81,31 +81,41 @@ export namespace Hypothesis {
export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => {
// if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client
// so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done
- !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
+ //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
var success = false;
const onSuccess = action(() => {
- console.log("Edit success!!");
+ console.log('Edit success!!');
success = true;
clearTimeout(interval);
- DocumentLinksButton.invisibleWebDoc = undefined;
- document.removeEventListener("editSuccess", onSuccess);
+ //DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener('editSuccess', onSuccess);
});
const newHyperlink = `[${title}\n](${url})`;
- const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful
- !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", {
- detail: { newHyperlink: newHyperlink, id: annotationId },
- bubbles: true
- })), 300);
-
- setTimeout(action(() => {
- if (!success) {
- clearInterval(interval);
- DocumentLinksButton.invisibleWebDoc = undefined;
- }
- }), 10000); // give up if no success after 10s
- document.addEventListener("editSuccess", onSuccess);
+ const interval = setInterval(
+ () =>
+ // keep trying to edit until annotations have loaded and editing is successful
+ !success &&
+ document.dispatchEvent(
+ new CustomEvent<{ newHyperlink: string; id: string }>('addLink', {
+ detail: { newHyperlink: newHyperlink, id: annotationId },
+ bubbles: true,
+ })
+ ),
+ 300
+ );
+
+ setTimeout(
+ action(() => {
+ if (!success) {
+ clearInterval(interval);
+ //DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }),
+ 10000
+ ); // give up if no success after 10s
+ document.addEventListener('editSuccess', onSuccess);
};
/**
@@ -114,33 +124,40 @@ export namespace Hypothesis {
export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => {
if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation
- !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
+ //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
var success = false;
const onSuccess = action(() => {
- console.log("Edit success!");
+ console.log('Edit success!');
success = true;
clearTimeout(interval);
- DocumentLinksButton.invisibleWebDoc = undefined;
- document.removeEventListener("editSuccess", onSuccess);
+ // DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener('editSuccess', onSuccess);
});
const annotationId = StrCast(linkDoc.annotationId);
const linkUrl = Doc.globalServerPath(sourceDoc);
- const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
- !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
- detail: { targetUrl: linkUrl, id: annotationId },
- bubbles: true
- }));
+ const interval = setInterval(() => {
+ // keep trying to edit until annotations have loaded and editing is successful
+ !success &&
+ document.dispatchEvent(
+ new CustomEvent<{ targetUrl: string; id: string }>('deleteLink', {
+ detail: { targetUrl: linkUrl, id: annotationId },
+ bubbles: true,
+ })
+ );
}, 300);
- setTimeout(action(() => {
- if (!success) {
- clearInterval(interval);
- DocumentLinksButton.invisibleWebDoc = undefined;
- }
- }), 10000); // give up if no success after 10s
- document.addEventListener("editSuccess", onSuccess);
+ setTimeout(
+ action(() => {
+ if (!success) {
+ clearInterval(interval);
+ //DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }),
+ 10000
+ ); // give up if no success after 10s
+ document.addEventListener('editSuccess', onSuccess);
};
/**
@@ -149,17 +166,20 @@ export namespace Hypothesis {
export const scrollToAnnotation = (annotationId: string, target: Doc) => {
var success = false;
const onSuccess = () => {
- console.log("Scroll success!!");
+ console.log('Scroll success!!');
document.removeEventListener('scrollSuccess', onSuccess);
clearInterval(interval);
success = true;
};
- const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
- document.dispatchEvent(new CustomEvent('scrollToAnnotation', {
- detail: annotationId,
- bubbles: true
- }));
+ const interval = setInterval(() => {
+ // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
+ document.dispatchEvent(
+ new CustomEvent('scrollToAnnotation', {
+ detail: annotationId,
+ bubbles: true,
+ })
+ );
const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
@@ -168,4 +188,4 @@ export namespace Hypothesis {
document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client
setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
};
-} \ No newline at end of file
+}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 37571ae01..559958c2b 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,27 +1,27 @@
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 { 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');
-const unsupported = ["text/html", "text/plain"];
+const unsupported = ['text/html', 'text/plain'];
@observer
export class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -29,7 +29,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 +40,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 +73,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 +81,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 +102,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) => {
@@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
modifiedDates.push(file.lastModified);
});
collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
- runInAction(() => this.completed += batch.length);
+ 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,7 +153,7 @@ 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) {
@@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
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 +176,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 +200,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 +208,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 +224,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
}
}
}
- }
+ };
render() {
const dimensions = 50;
@@ -228,193 +235,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/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 289c5bc51..9591dbed3 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,12 +1,13 @@
-import React = require("react");
-import { Utils } from "../../Utils";
-import "./InteractionUtils.scss";
+import React = require('react');
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { Utils } from '../../Utils';
+import './InteractionUtils.scss';
export namespace InteractionUtils {
- export const MOUSETYPE = "mouse";
- export const TOUCHTYPE = "touch";
- export const PENTYPE = "pen";
- export const ERASERTYPE = "eraser";
+ export const MOUSETYPE = 'mouse';
+ export const TOUCHTYPE = 'touch';
+ export const PENTYPE = 'pen';
+ export const ERASERTYPE = 'eraser';
const POINTER_PEN_BUTTON = -1;
const REACT_POINTER_PEN_BUTTON = 0;
@@ -19,24 +20,23 @@ export namespace InteractionUtils {
readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
- ) { }
+ ) {}
}
- export interface MultiTouchEventDisposer { (): void; }
+ export interface MultiTouchEventDisposer {
+ (): void;
+ }
/**
*
* @param element - element to turn into a touch target
* @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
*/
- export function MakeMultiTouchTarget(
- element: HTMLElement,
- startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
// const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
// const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
- element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -44,7 +44,7 @@ export namespace InteractionUtils {
// element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
// }
return () => {
- element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -59,14 +59,11 @@ export namespace InteractionUtils {
* @param element - element to add events to
* @param func - function to add to the event
*/
- export function MakeHoldTouchTarget(
- element: HTMLElement,
- func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
- element.addEventListener("dashOnTouchHoldStart", handler);
+ element.addEventListener('dashOnTouchHoldStart', handler);
return () => {
- element.removeEventListener("dashOnTouchHoldStart", handler);
+ element.removeEventListener('dashOnTouchHoldStart', handler);
};
}
@@ -89,71 +86,115 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
- downHdlr?: ((e: React.PointerEvent) => void)) {
+ export function CreatePolyline(
+ points: { X: number; Y: number }[],
+ left: number,
+ top: number,
+ color: string,
+ width: number,
+ strokeWidth: number,
+ lineJoin: string,
+ lineCap: string,
+ bezier: string,
+ fill: string,
+ arrowStart: string,
+ arrowEnd: string,
+ markerScale: number,
+ dash: string | undefined,
+ scalex: number,
+ scaley: number,
+ shape: string,
+ pevents: string,
+ opacity: number,
+ nodefs: boolean,
+ downHdlr?: (e: React.PointerEvent) => void,
+ mask?: boolean
+ ) {
const pts = shape ? makePolygon(shape, points) : points;
if (isNaN(scalex)) scalex = 1;
if (isNaN(scaley)) scaley = 1;
- const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
- const strpts = bezier ?
- pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
- pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
+ const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
+ const strpts = bezier
+ ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '')
+ : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, '');
const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
const defGuid = Utils.GenerateGuid();
- const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
+ const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements;
const markerStrokeWidth = strokeWidth / 2;
- const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5
+ const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
- return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
- {nodefs ? (null) : <defs>
- {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" />
- </marker>}
- {arrowStart !== "arrow" ? (null) :
- <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} />
- </marker>}
- {arrowEnd !== "arrow" ? (null) :
- <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} />
- </marker>}
- </defs>}
-
- <Tag
- d={bezier ? strpts : undefined}
- points={bezier ? undefined : strpts}
- style={{
- // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
- fill: fill && fill !== "transparent" ? fill : "none",
- opacity: 1.0,
- // opacity: strokeWidth !== width ? 0.5 : undefined,
- pointerEvents: pevents as any,
- stroke: color ?? "rgb(0, 0, 0)",
- strokeWidth: strokeWidth,
- strokeLinecap: lineCap as any,
- strokeDasharray: dashArray
- }}
- markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`}
- markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`}
- />
-
- </svg>);
+ return (
+ <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ {' '}
+ {/* 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" />
+ </marker>
+ )}
+ {arrowStart !== 'arrow' ? null : (
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${
+ markerStrokeWidth * arrowWidthFactor
+ }, 0 0`}
+ />
+ </marker>
+ )}
+ {arrowEnd !== 'arrow' ? null : (
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
+ />
+ </marker>
+ )}
+ </defs>
+ )}
+ <Tag
+ d={bezier ? strpts : undefined}
+ points={bezier ? undefined : strpts}
+ 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,
+ stroke: color ?? 'rgb(0, 0, 0)',
+ strokeWidth: strokeWidth,
+ strokeLinecap: lineCap as any,
+ strokeDasharray: dashArray,
+ transition: 'inherit',
+ }}
+ markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`}
+ markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`}
+ />
+ </svg>
+ );
}
- export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
+ export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
//pointer is up (first and last points are the same)
- if (shape === "arrow" || shape === "line" || shape === "circle") {
+ if (shape === GestureUtils.Gestures.Arrow || shape === GestureUtils.Gestures.Line || shape === GestureUtils.Gestures.Circle) {
//if arrow or line, the two end points should be the starting and the ending point
var left = points[0].X;
var top = points[0].Y;
@@ -175,7 +216,7 @@ export namespace InteractionUtils {
left = points[0].X;
bottom = points[points.length - 1].Y;
top = points[0].Y;
- if (shape !== "arrow" && shape !== "line" && shape !== "circle") {
+ if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) {
//switch left/right and top/bottom if needed
if (left > right) {
const temp = right;
@@ -191,90 +232,41 @@ export namespace InteractionUtils {
}
points = [];
switch (shape) {
- case "rectangle":
+ case GestureUtils.Gestures.Rectangle:
points.push({ X: left, Y: top });
points.push({ X: right, Y: top });
points.push({ X: right, Y: bottom });
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: top });
- return points;
- case "triangle":
- // points.push({ X: left, Y: bottom });
- // points.push({ X: right, Y: bottom });
- // points.push({ X: (right + left) / 2, Y: top });
- // points.push({ X: left, Y: bottom });
-
+ break;
+ case GestureUtils.Gestures.Triangle:
points.push({ X: left, Y: bottom });
- points.push({ X: left, Y: bottom });
-
- points.push({ X: right, Y: bottom });
points.push({ X: right, Y: bottom });
- points.push({ X: right, Y: bottom });
- points.push({ X: right, Y: bottom });
-
- points.push({ X: (right + left) / 2, Y: top });
- points.push({ X: (right + left) / 2, Y: top });
- points.push({ X: (right + left) / 2, Y: top });
points.push({ X: (right + left) / 2, Y: top });
-
- points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
-
-
- return points;
- case "circle":
+ break;
+ case GestureUtils.Gestures.Circle:
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
- if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
- for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- points.push({ X: x, Y: y });
- }
- for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- const newX = centerX - (x - centerX);
- points.push({ X: newX, Y: y });
- }
- points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
- } else {
- for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- points.push({ X: x, Y: y });
- }
- for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- const newY = centerY - (y - centerY);
- points.push({ X: x, Y: newY });
- }
- points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
+ for (var x = centerX - radius; x < centerX + radius; x++) {
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
+ points.push({ X: x, Y: y });
+ }
+ for (var x = centerX + radius; x > centerX - radius; x--) {
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
+ const newY = centerY - (y - centerY);
+ points.push({ X: x, Y: newY });
}
- return points;
- // case "arrow":
- // const x1 = left;
- // const y1 = top;
- // const x2 = right;
- // const y2 = bottom;
- // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
- // const L2 = L1 / 5;
- // const angle = 0.785398;
- // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
- // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
- // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
- // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- // points.push({ X: x1, Y: y1 });
- // points.push({ X: x2, Y: y2 });
- // points.push({ X: x3, Y: y3 });
- // points.push({ X: x4, Y: y4 });
- // points.push({ X: x2, Y: y2 });
- // return points;
- case "line":
+ points.push({ X: centerX - radius, Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(-radius, 2)) + centerY });
+ break;
+
+ case GestureUtils.Gestures.Line:
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
- return points;
- default:
- return points;
+ break;
}
+ return points;
}
/**
* Returns whether or not the pointer event passed in is of the type passed in
@@ -282,14 +274,14 @@ export namespace InteractionUtils {
* @param type - InteractionUtils.(PENTYPE | ERASERTYPE | MOUSETYPE | TOUCHTYPE)
*/
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
+ // prettier-ignore
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
- case TOUCHTYPE:
- return e.pointerType === TOUCHTYPE;
- default: return e.pointerType === type;
+ case TOUCHTYPE: return e.pointerType === TOUCHTYPE;
}
+ return e.pointerType === type;
}
/**
@@ -305,7 +297,7 @@ export namespace InteractionUtils {
* Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
* @param pts - n-arbitrary long list of points
*/
- export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+ export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } {
const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
return { X: centerX, Y: centerY };
@@ -324,9 +316,9 @@ export namespace InteractionUtils {
const newDist = TwoPointEuclidist(pt1, pt2);
/** if they have the same sign, then we are either pinching in or out.
- * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
- * so that it can still pan without freaking out
- */
+ * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+ * so that it can still pan without freaking out
+ */
if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
return Math.sign(oldDist - newDist);
}
@@ -372,8 +364,6 @@ export namespace InteractionUtils {
// These might not be very useful anymore, but I'll leave them here for now -syip2
{
-
-
/**
* Returns the type of Touch Interaction from a list of points.
* Also returns any data that is associated with a Touch Interaction
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f75ac24f5..df61ecece 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,15 +1,14 @@
-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, runInAction } from 'mobx';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentDecorations } from '../views/DocumentDecorations';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } from '../views/nodes/DocumentView';
+import { PresBox } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
+import { LinkManager } from './LinkManager';
+import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
/*
* link doc:
* - anchor1: doc
@@ -26,84 +25,93 @@ export class LinkFollower {
// follows a link - if the target is on screen, it highlights/pans to it.
// if the target isn't onscreen, then it will open up the target in the lightbox, or in place
// depending on the followLinkLocation property of the source (or the link itself as a fallback);
- public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
+ public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
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);
+ runInAction(() => (DocumentDecorations.Instance.overrideBounds = 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(() => (DocumentDecorations.Instance.overrideBounds = 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);
+ public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
+ const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
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 fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
+ const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (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);
+ const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
+ 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.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) {
+ 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),
+ 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.context);
+ if (target.context instanceof Doc && target.context !== sourceDocParent) {
+ Doc.RemoveDocFromList(target.context, Doc.LayoutFieldKey(target.context), target);
+ movedTarget = true;
+ }
+ if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) {
+ Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
+ movedTarget = true;
+ }
+ target.context = 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;
}
- } else {
- allFinished();
- }
+ if (movedTarget) setTimeout(doFollow);
+ else doFollow(true);
+ } else doFollow(true);
} else {
allFinished();
}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 7a12a8580..555215417 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -3,7 +3,8 @@ import { computedFn } from 'mobx-utils';
import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
import { List } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
-import { Cast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { ScriptingGlobals } from './ScriptingGlobals';
/*
* link doc:
* - anchor1: doc
@@ -19,34 +20,47 @@ 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?.anchor1), PromiseValue(link?.anchor2)]
+ )
+ );
+ LinkManager.userLinkDBs.push(linkDb);
+ };
public static AutoKeywords = 'keywords:Usages';
- static links: Doc[] = [];
+ static _links: Doc[] = [];
constructor() {
LinkManager._instance = this;
this.createLinkrelationshipLists();
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.anchor1, link.anchor2]).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)?.[DirectLinksSym].add(link);
+ (protos[1] as Doc)?.[DirectLinksSym].add(link);
+ })
+ );
+ });
+ });
});
};
const remLinkFromDoc = (link: Doc) => {
@@ -63,41 +77,43 @@ export class LinkManager {
);
};
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,13 +128,6 @@ 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 = () => {
@@ -144,28 +153,18 @@ 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)[DirectLinksSym] ?? []);
} // 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 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
@@ -199,3 +198,11 @@ export class LinkManager {
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/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 86bc4c5de..d5bffc5e2 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,22 +1,24 @@
-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 { 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 { OpenWhereMod } from '../views/nodes/DocumentView';
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,20 +39,27 @@ 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; }
-
+ if (presentation == null) {
+ console.warn('setVideoBox on null videoBox presentation');
+ return;
+ }
+
let docIdtoDoc: Map<string, Doc> = new Map();
try {
docIdtoDoc = await this.loadPresentation(presentation);
@@ -59,29 +68,30 @@ export class ReplayMovements {
throw 'error loading docs from server';
}
-
- this.videoBoxDisposeFunc =
- reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
- ({ playing, timeViewed }) =>
- playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
- );
+ this.videoBoxDisposeFunc = reaction(
+ () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+ ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements())
+ );
this.videoBox = videoBox;
- }
+ };
removeVideoBox = () => {
- if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+ if (this.videoBoxDisposeFunc == null) {
+ console.warn('removeVideoBox on null videoBox');
+ return;
+ }
this.videoBoxDisposeFunc();
this.videoBox = null;
this.videoBoxDisposeFunc = null;
- }
+ };
// should be called from interacting with the screen
pauseFromInteraction = () => {
this.videoBox?.Pause();
this.pauseMovements();
- }
+ };
loadPresentation = async (presentation: Presentation) => {
const { movements } = presentation;
@@ -91,7 +101,7 @@ export class ReplayMovements {
// generate a set of all unique docIds
const docIds = new Set<string>();
- for (const {docId} of movements) {
+ for (const { docId } of movements) {
if (!docIds.has(docId)) docIds.add(docId);
}
@@ -107,27 +117,29 @@ export class ReplayMovements {
// 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; }
- }
+ 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}`)
+ console.error(`docIdtoDoc did not contain docId ${docId}`);
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) => {
@@ -135,7 +147,7 @@ export class ReplayMovements {
scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
document.Document._panX = panX;
document.Document._panY = panY;
- }
+ };
getFirstMovements = (movements: Movement[]): Map<string, Movement> => {
if (movements === null) return new Map();
@@ -146,18 +158,19 @@ export class ReplayMovements {
if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
}
return docIdtoFirstMove;
- }
+ };
endPlayingPresentation = () => {
this.isPlaying = false;
Doc.UserDoc().presentationMode = 'none';
- }
+ };
public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
// console.info('playMovements', presentation, timeViewed, docIdtoDoc);
- if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
- return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
+ if (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,7 +178,7 @@ 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
@@ -179,13 +192,12 @@ export class ReplayMovements {
const colFFView = this.getCollectionFFView(docId);
if (colFFView) this.zoomAndPan(firstMove, colFFView);
}
- }
+ };
handleFirstMovements();
-
// make timers that will execute each movement at the correct replay time
this.timers = filteredMovements.map(movement => {
- const timeDiff = movement.time - timeViewed * 1000
+ const timeDiff = movement.time - timeViewed * 1000;
return setTimeout(() => {
const collectionFFView = this.getCollectionFFView(movement.docId);
@@ -204,5 +216,5 @@ export class ReplayMovements {
}
}, timeDiff);
});
- }
+ };
}
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
new file mode 100644
index 000000000..5a2f2fcad
--- /dev/null
+++ b/src/client/util/ReportManager.scss
@@ -0,0 +1,88 @@
+@import '../views/global/globalCssVariables';
+
+.issue-list-wrapper {
+ position: relative;
+ min-width: 250px;
+ background-color: $light-blue;
+ overflow-y: scroll;
+}
+
+.issue-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+ margin: 5px;
+ border-radius: 5px;
+ border: 1px solid grey;
+ background-color: lightgoldenrodyellow;
+}
+
+// issue should pop up when the user hover over the issue
+.issue-list:hover {
+ box-shadow: 2px;
+ cursor: pointer;
+ border: 3px solid #252b33;
+}
+
+.issue-content {
+ background-color: white;
+ padding: 10px;
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.issue-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: black;
+}
+
+.issue-body {
+ padding: 0 10px;
+ width: 100%;
+ text-align: left;
+}
+
+.issue-body > * {
+ margin-top: 5px;
+}
+
+.issue-body img,
+.issue-body video {
+ display: block;
+ max-width: 100%;
+}
+
+.report-issue-fab {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.loading-center {
+ margin: auto 0;
+}
+
+.settings-content label {
+ margin-top: 10px;
+}
+
+.report-disclaimer {
+ font-size: 8px;
+ color: grey;
+ padding-right: 50px;
+ font-style: italic;
+ text-align: left;
+}
+
+.flex-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
new file mode 100644
index 000000000..51742d455
--- /dev/null
+++ b/src/client/util/ReportManager.tsx
@@ -0,0 +1,297 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
+import { DocServer } from '../DocServer';
+import { Networking } from '../Network';
+import { MainViewModal } from '../views/MainViewModal';
+import { FontIconBox } from '../views/nodes/button/FontIconBox';
+import { DragManager } from './DragManager';
+import { GroupManager } from './GroupManager';
+import './SettingsManager.scss';
+import './ReportManager.scss';
+import { undoBatch } from './UndoManager';
+import { Octokit } from "@octokit/core";
+import { CheckBox } from '../views/search/CheckBox';
+import ReactLoading from 'react-loading';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ private octokit: Octokit;
+
+ @observable public issues: any[] = [];
+ @action setIssues = action((issues: any[]) => { this.issues = issues; });
+
+ // undefined is the default - null is if the user is making an issue
+ @observable public selectedIssue: any = undefined;
+ @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
+
+ // only get the open issues
+ @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
+
+ public updateIssueSearch = action((query: string = '') => {
+ if (query === '') {
+ this.shownIssues = this.issues.filter(issue => issue.state === 'open');
+ return;
+ }
+ this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ this.octokit = new Octokit({
+ auth: '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'
+ });
+ }
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(() => {
+ if (this.issues.length === 0) {
+ // load in the issues if not already loaded
+ this.getAllIssues()
+ .then(issues => {
+ this.setIssues(issues);
+ this.updateIssueSearch();
+ })
+ .catch(err => console.log(err));
+ }
+ (this.isOpen = true)
+ });
+
+ @observable private bugTitle = '';
+ @action setBugTitle = action((title: string) => { this.bugTitle = title; });
+ @observable private bugDescription = '';
+ @action setBugDescription = action((description: string) => { this.bugDescription = description; });
+ @observable private bugType = '';
+ @action setBugType = action((type: string) => { this.bugType = type; });
+ @observable private bugPriority = '';
+ @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
+
+ // private toGithub = false;
+ // will always be set to true - no alterntive option yet
+ private toGithub = true;
+
+ private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+ public async getAllIssues() : Promise<any[]> {
+ const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+ }
+
+ // turns an upload link into a servable link
+ // ex:
+ // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
+ // -> http://localhost:1050/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
+ private fileLinktoServerLink = (fileLink: string) => {
+ const serverUrl = 'https://browndash.com/';
+
+ const regex = 'public'
+ const publicIndex = fileLink.indexOf(regex) + regex.length;
+
+ const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
+ return finalUrl;
+ }
+
+ public async reportIssue() {
+ if (this.bugTitle === '' || this.bugDescription === ''
+ || this.bugType === '' || this.bugPriority === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+
+ if (this.toGithub) {
+
+ const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink)
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
+ body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
+ labels: [
+ 'from-dash-app',
+ this.bugType,
+ this.bugPriority
+ ]
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ // on error, don't close the modal
+ return;
+ }
+ }
+ else {
+ // if not going to github issues, not sure what to do yet...
+ }
+
+ // if we're down here, then we're good to go. reset the fields.
+ this.setBugTitle('');
+ this.setBugDescription('');
+ // this.toGithub = false;
+ this.setFileLinks([]);
+ this.setBugType('');
+ this.setBugPriority('');
+ this.close();
+ }
+
+ @observable public fileLinks: any = [];
+ @action setFileLinks = action((links: any) => { this.fileLinks = links; });
+
+ private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
+
+ private uploadFiles = (input: any) => {
+ // keep null while uploading
+ this.setFileLinks(null);
+ // 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 => {
+ console.log('finshed uploading', links.map(this.getServerPath));
+ this.setFileLinks((links ?? []).map(this.getServerPath));
+ })
+ }
+
+ }
+
+
+ private renderIssue = (issue: any) => {
+
+ const isReportingIssue = issue === null;
+
+ return isReportingIssue ?
+ // report issue
+ (<div className="settings-content">
+ <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
+ <label>Please leave a title for the bug.</label><br />
+ <input type="text" placeholder='title' onChange={(e) => this.setBugTitle(e.target.value)} required/>
+ <br />
+ <label>Please leave a description for the bug and how it can be recreated.</label>
+ <textarea placeholder='description' onChange={(e) => this.setBugDescription(e.target.value)} required/>
+ <br />
+ {/* {<label>Send to github issues? </label>
+ <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
+ <br /> } */}
+
+ <label>Please label the issue</label>
+ <div className='flex-select'>
+ <select name="bugType" onChange={e => this.bugType = e.target.value}>
+ <option value="" disabled selected>Type</option>
+ <option value="bug">Bug</option>
+ <option value="cosmetic">Poor Design or Cosmetic</option>
+ <option value="documentation">Poor Documentation</option>
+ </select>
+
+ <select name="bigPriority" onChange={e => this.bugPriority = e.target.value}>
+ <option value="" disabled selected>Priority</option>
+ <option value="priority-low">Low</option>
+ <option value="priority-medium">Medium</option>
+ <option value="priority-high">High</option>
+ </select>
+ </div>
+
+
+ <div>
+ <label>Upload media that shows the bug (optional)</label>
+ <input type="file" name="file" multiple accept='audio/*, video/*, image/*' onChange={e => this.uploadFiles(e.target)}/>
+ </div>
+ <br />
+
+ <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
+ </div>)
+ :
+ // view issue
+ (
+ <div className='issue-container'>
+ <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
+ <div className='issue-title'>
+ {issue.title}
+ </div>
+ <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+ }
+
+ private showReportIssueScreen = () => {
+ this.setSelectedIssue(null);
+ }
+
+ private closeReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ }
+
+ private get reportInterface() {
+
+ const isReportingIssue = this.selectedIssue === null;
+
+ return (
+ <div className="settings-interface">
+ <div className='issue-list-wrapper'>
+ <h3>Current Issues</h3>
+ <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
+ {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
+
+ {/* <div className="settings-user">
+ <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
+ </div> */}
+ </div>
+
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+
+ <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
+ {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
+ </div>
+
+ <div className='report-issue-fab'>
+ <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
+ <button
+ onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
+ >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
+ </div>
+
+
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ />
+ );
+ }
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index ea2bf6551..f17a98616 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,6 +7,10 @@
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import * as ts from 'typescript';
import { Doc, Field } from '../../fields/Doc';
+import { ToScriptString } from '../../fields/FieldSymbols';
+import { ObjectField } from '../../fields/ObjectField';
+import { RefField } from '../../fields/RefField';
+import { ScriptField } from '../../fields/ScriptField';
import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
@@ -177,6 +181,14 @@ 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) => {
+ 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;
if (options.params && !options.params.this) options.params.this = Doc.name;
if (options.params && !options.params.self) options.params.self = Doc.name;
@@ -241,7 +253,11 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (options.globals) {
ScriptingGlobals.resetScriptingGlobals();
}
+ !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
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/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1c84af94a..313c255a0 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,15 +1,18 @@
+import { ModalManager } from '@material-ui/core';
import { action, observable, ObservableMap } from 'mobx';
import { computedFn } from 'mobx-utils';
import { Doc, Opt } from '../../fields/Doc';
import { DocCast } from '../../fields/Types';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { DocumentView } from '../views/nodes/DocumentView';
+import { 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,38 +22,46 @@ 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) && docView.props.Document.type !== DocumentType.MARKER) {
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 {
@@ -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,16 +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)?._viewType !== 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?._viewType !== CollectionViewType.Docking);
+ // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
}
}
-ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
+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;
+ return selected?.type === type || selected?.viewType === 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/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 cf143c5e8..396d754b6 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -26,11 +26,16 @@ export enum ColorScheme {
System = '-MatchSystem',
}
+export enum freeformScrollMode {
+ Pan = 'pan',
+ Zoom = 'zoom',
+}
+
@observer
export class SettingsManager extends React.Component<{}> {
public static Instance: SettingsManager;
static _settingsStyle = addStyleSheet();
- @observable private isOpen = false;
+ @observable public isOpen = false;
@observable private passwordResultText = '';
@observable private playgroundMode = false;
@@ -179,13 +184,21 @@ 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.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>
);
}
@@ -298,6 +311,10 @@ export class SettingsManager extends React.Component<{}> {
);
}
+ setFreeformScrollMode = (mode: freeformScrollMode) => {
+ Doc.UserDoc().freeformScrollMode = mode;
+ };
+
@computed get modesContent() {
return (
<div className="tab-content modes-content">
@@ -319,6 +336,21 @@ export class SettingsManager extends React.Component<{}> {
<div className="playground-text">Playground Mode</div>
</div>
</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">
<div className="tab-column-title">Permissions</div>
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 2de636f21..932e94664 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -107,7 +107,7 @@
.user-sort {
text-align: left;
margin-left: 10;
- width: 100px;
+ width: 100%;
cursor: pointer;
}
@@ -122,6 +122,7 @@
background: #e8e8e8;
padding-left: 10px;
padding-right: 10px;
+ width: 100%;
overflow-y: scroll;
overflow-x: hidden;
text-align: left;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 895bd3374..3c05af4bb 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,9 +5,10 @@ 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 { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping, Opt } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, NumCast, PromiseValue, StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
import { Utils } from '../../Utils';
import { DocServer } from '../DocServer';
@@ -20,10 +21,9 @@ import { SearchBox } from '../views/search/SearchBox';
import { DocumentManager } from './DocumentManager';
import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
+import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
import './SharingManager.scss';
-import { LinkManager } from './LinkManager';
-import { Id } from '../../fields/FieldSymbols';
export interface User {
email: string;
@@ -82,16 +82,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 +130,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 +160,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[DataSym]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -217,6 +194,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[DataSym]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -283,6 +261,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 +269,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])))) {
+ 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,9 +355,9 @@ 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.View, SharingPermissions.SelfEdit].includes(permission as any))
+ .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
.map(permission => (
<option key={permission} value={permission}>
{permission}
@@ -392,7 +375,7 @@ export class SharingManager extends React.Component<{}> {
onClick={() => {
let context: Opt<CollectionView>;
if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) {
- DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]);
+ DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
}
}}
onPointerEnter={action(() => {
@@ -458,17 +441,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 +463,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
@@ -523,21 +496,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]?.[DataSym];
// 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[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[AclSym])));
// the list of users shared with
const userListContents: (JSX.Element | null)[] = users
.filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
.map(({ user, linkDatabase, sharingDoc, userColor }) => {
const userKey = `acl-${normalizeEmail(user.email)}`;
- const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]));
+ const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]);
const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
return !permissions ? null : (
@@ -573,7 +546,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 +557,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[DataSym]))
+ .every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
return !permissions ? null : (
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.ts
index beb030635..57e8516ac 100644
--- a/src/client/util/request-image-size.js
+++ b/src/client/util/request-image-size.ts
@@ -13,17 +13,20 @@ 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
+ encoding: null,
};
if (options && typeof options === 'object') {
opts = Object.assign(options, opts);
} else if (options && typeof options === 'string') {
- opts = Object.assign({
- uri: options
- }, opts);
+ opts = Object.assign(
+ {
+ uri: options,
+ },
+ opts
+ );
} else {
return Promise.reject(new Error('You should provide an URI string or a "request" options object.'));
}
@@ -33,36 +36,31 @@ 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 = new Buffer.from([]);
- let size;
- let imageSizeError;
+ let buffer = Buffer.from([]);
+ let size: any;
- res.on('data', chunk => {
+ res.on('data', (chunk: any) => {
buffer = Buffer.concat([buffer, chunk]);
try {
size = imageSize(buffer);
- } catch (err) {
- imageSizeError = err;
- return;
- }
-
- if (size) {
- resolve(size);
- return req.abort();
- }
+ if (size) {
+ resolve(size);
+ return req.abort();
+ }
+ } catch (err) {}
});
- res.on('error', err => reject(err));
+ res.on('error', reject);
res.on('end', () => {
if (!size) {
- return reject(imageSizeError);
+ return reject(new Error('Image has no size'));
}
size.downloaded = buffer.length;
@@ -70,6 +68,6 @@ module.exports = function requestImageSize(options) {
});
});
- req.on('error', err => reject(err));
+ req.on('error', reject);
});
-}; \ No newline at end of file
+};
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 9063dc894..1a93bbe59 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -67,6 +67,9 @@ interface RegExp {
readonly sticky: boolean;
readonly unicode: boolean;
}
+interface Date {
+ now() : string;
+}
interface String {
codePointAt(pos: number): number | undefined;
includes(searchString: string, position?: number): boolean;