diff options
Diffstat (limited to 'src/client/util')
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; |
