diff options
Diffstat (limited to 'src/client/util')
23 files changed, 799 insertions, 848 deletions
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx index 8451357ef..2939ba581 100644 --- a/src/client/util/CaptureManager.tsx +++ b/src/client/util/CaptureManager.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { addStyleSheet } from '../../Utils'; +import { addStyleSheet } from '../../ClientUtils'; import { Doc } from '../../fields/Doc'; import { DocCast, StrCast } from '../../fields/Types'; import { LightboxView } from '../views/LightboxView'; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 081115879..7811f8605 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,6 +1,6 @@ import { observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; -import { OmitKeys, Utils } from "../../Utils"; +import { ClientUtils, OmitKeys } from "../../ClientUtils"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { DocData } from "../../fields/DocSymbols"; import { InkTool } from "../../fields/InkField"; @@ -12,7 +12,7 @@ import { ScriptField } from "../../fields/ScriptField"; import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types"; import { WebField, nullAudio } from "../../fields/URLField"; import { SetCachedGroups, SharingPermissions } from "../../fields/util"; -import { GestureUtils } from "../../pen-gestures/GestureUtils"; +import { Gestures } from "../../pen-gestures/GestureTypes"; import { DocServer } from "../DocServer"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; import { DocUtils, Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents"; @@ -20,20 +20,21 @@ import { DashboardView } from "../views/DashboardView"; import { OverlayView } from "../views/OverlayView"; import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView"; import { Colors } from "../views/global/globalEnums"; -import { media_state } from "../views/nodes/AudioBox"; +import { mediaState } from "../views/nodes/AudioBox"; import { OpenWhere } from "../views/nodes/DocumentView"; import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox"; +import { ImageBox } from "../views/nodes/ImageBox"; +import { LabelBox } from "../views/nodes/LabelBox"; import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox"; -import { DragManager, dropActionType } from "./DragManager"; +import { DragManager } from "./DragManager"; +import { dropActionType } from "./DropActionTypes"; import { MakeTemplate } from "./DropConverter"; import { FollowLinkScript } from "./LinkFollower"; -import { LinkManager } from "./LinkManager"; +import { LinkManager, UPDATE_SERVER_CACHE } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; import { ColorScheme } from "./SettingsManager"; import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; -import { LabelBox } from "../views/nodes/LabelBox"; -import { ImageBox } from "../views/nodes/ImageBox"; interface Button { // DocumentOptions fields a button can set @@ -60,6 +61,7 @@ interface Button { subMenu?: Button[]; } +// eslint-disable-next-line import/no-mutable-exports export let resolvedPorts: { server: number, socket: number }; export class CurrentUserUtils { @@ -90,6 +92,7 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true}; + // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } @@ -114,6 +117,7 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true}; + // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } @@ -131,6 +135,7 @@ export class CurrentUserUtils { }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; + // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } static setupUserTemplates(doc: Doc, field="template_user") { @@ -138,6 +143,7 @@ export class CurrentUserUtils { const reqdUserList = DocListCast(tempUsers?.data); const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true }; + // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts)); } @@ -159,24 +165,26 @@ export class CurrentUserUtils { const reqdOpts = { title: "icon templates", _height: 75, isSystem: true }; const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); + const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ + layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + }); + const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); + const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); + const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => { const title = "icon" + (type ? "_" + type : ""); const curIcon = DocCast(templateIconsDoc[title]); - let creator = labelBox; - switch (opts.iconTemplate) { - case DocumentType.IMG : creator = imageBox; break; - case DocumentType.FONTICON: creator = fontBox; break; - } + const creator = (() => { switch (opts.iconTemplate) { + case DocumentType.IMG : return imageBox; + case DocumentType.FONTICON: return fontBox; + default: return labelBox; + }})(); const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts, title}; + // eslint-disable-next-line no-return-assign return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[title] = MakeTemplate(creator(allopts, templateField)))), {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", }); }; - const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ - layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts - }); - const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); - const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); const iconTemplates = [ makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}), makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}), @@ -188,9 +196,9 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.BUTTON,"title", { 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 + // nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }), - //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts)) + // makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts)) ].filter(d => d).map(d => d!); DocUtils.AssignOpts(DocCast(doc[field]), {}, iconTemplates); } @@ -241,14 +249,15 @@ export class CurrentUserUtils { Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) ], {...opts, title: "Slide View Template"})); const plotlyApi = () => { - var plotly = Doc.MyPublishedDocs.find(doc => doc.title === "@plotly"); + let plotly = Doc.MyPublishedDocs.find(doc => doc.title === "@plotly"); if (!plotly) { - const plotly = Docs.Create.TextDocument( + plotly = Docs.Create.TextDocument( `await import("https://cdn.plot.ly/plotly-2.27.0.min.js"); Plotly.newPlot(dashDiv.id, [ --DOCDATA-- ])` , {title: "@plotly", title_custom: true, _layout_showTitle:"title", _width:300,_height:400}); Doc.AddToMyPublished(plotly); } + return plotly; } const plotlyView = (opts:DocumentOptions) => { const rtfield = new RichTextField(JSON.stringify( @@ -280,10 +289,10 @@ export class CurrentUserUtils { return slide; } const mermaidsApi = () => { - var mermaids = Doc.MyPublishedDocs.find(doc => doc.title === "@mermaids"); + let mermaids = Doc.MyPublishedDocs.find(doc => doc.title === "@mermaids"); if (!mermaids) { - const mermaids = Docs.Create.TextDocument( - `const mdef = (await import("https://cdn.jsdelivr.net/npm/mermaid\@10.8.0/dist/mermaid.esm.min.mjs")).default; + mermaids = Docs.Create.TextDocument( + `const mdef = (await import("https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.esm.min.mjs")).default; window["callb"] = (x) => { alert(x); } @@ -301,6 +310,7 @@ export class CurrentUserUtils { , {title: "@mermaids", title_custom: true, _layout_showTitle:"title", _width:300,_height:400}); Doc.AddToMyPublished(mermaids); } + return mermaids; } const mermaidsView = (opts:DocumentOptions) => { const rtfield = new RichTextField(JSON.stringify( @@ -333,7 +343,7 @@ pie title Minerals in my tap water slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"}); return slide; } - const apis = [plotlyApi(), mermaidsApi()] + plotlyApi(); mermaidsApi(); const emptyThings:{key:string, // the field name where the empty thing will be stored opts:DocumentOptions, // the document options that are required for the empty thing funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing @@ -465,83 +475,9 @@ pie title Minerals in my tap water return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); } - // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu - static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") { - const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, isSystem: true, _chromeHidden: true,}; - DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts); - } - - // Sets up mobile buttons for inside mobile menu - static setupMobileButtons(doc?: Doc, buttons?: string[]) { - return []; - const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [ - { title: "DASHBOARDS", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Dashboards from your mobile, and navigate through all of your documents. " }, - { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." }, - { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." }, - { title: "RECORD", icon: "microphone", click: 'openMobileAudio()', backgroundColor: "lightgrey", info: "Use your phone to record, dictate and then upload audio onto Dash Web." }, - { title: "PRESENTATION", icon: "desktop", click: 'switchToMobilePresentation()', backgroundColor: "lightgrey", info: "Use your phone as a remote for you presentation." }, - { title: "SETTINGS", icon: "cog", click: 'openMobileSettings()', backgroundColor: "lightgrey", info: "Change your password, log out, or manage your account security." } - ]; - // returns a list of mobile buttons - return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => - this.mobileButton({ - title: data.title, - _lockedPosition: true, - onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - backgroundColor: data.backgroundColor, isSystem: true - }, - [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", isSystem: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) - ); - } - - // sets up the main document for the mobile button - static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { - ...opts, - _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, - layout_borderRounding: "5px", layout_boxShadow: "0 0", isSystem: true - }) as any as Doc - - // sets up the text container for the information contained within the mobile button - static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { - ...opts, - _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, - backgroundColor: "rgba(0,0,0,0)", layout_borderRounding: "0", layout_boxShadow: "0 0", ignoreClick: true, isSystem: true - }) as any as Doc - - // Sets up the title of the button - static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { - ...opts, - title: buttonTitle, _text_fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true - }) as any as Doc - - // Sets up the description of the button - static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { - ...opts, - title: "info", _text_fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true - }) as any as Doc - - - - static setupMobileInkingDoc(userDoc: Doc) { - return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", isSystem: true }); - } - - static setupMobileUploadDoc(userDoc: Doc) { - // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" }) - const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", { - title: "Upload Images From the Web", _lockedPosition: true, isSystem: true - }); - const uploadDoc = Docs.Create.StackingDocument([], { - title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, isSystem: true, _chromeHidden: true, - }); - return Docs.Create.StackingDocument([webDoc, uploadDoc], { - _width: screen.width, _lockedPosition: true, title: "Upload", _layout_autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", isSystem: true, _chromeHidden: true, - }); - } - /// Search option on the left side button panel static setupSearcher(doc: Doc, field:string) { - return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.SearchDocument(opts), { dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: dropActionType.embed, _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } @@ -552,7 +488,7 @@ pie title Minerals in my tap water const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined); const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined; const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools); - //doc.myUserBtns = new PrefetchProxy(userBtns); + // doc.myUserBtns = new PrefetchProxy(userBtns); const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", layout_explainer: "This is a palette of documents that can be created.", @@ -563,7 +499,7 @@ pie title Minerals in my tap water /// initializes the left sidebar dashboard pane static setupDashboards(doc: Doc, field:string) { - var myDashboards = DocCast(doc[field]); + let myDashboards = DocCast(doc[field]); const newDashboard = `createNewDashboard()`; @@ -572,9 +508,9 @@ pie title Minerals in my tap water const reqdBtnScript = {onClick: newDashboard,} const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); - const contextMenuScripts = [/*newDashboard*/] as string[]; - const contextMenuLabels = [/*"Create New Dashboard"*/] as string[]; - const contextMenuIcons = [/*"plus"*/] as string[]; + const contextMenuScripts = [/* newDashboard */] as string[]; + const contextMenuLabels = [/* "Create New Dashboard" */] as string[]; + const contextMenuIcons = [/* "plus" */] as string[]; const childContextMenuScripts = [`toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(this)`, 'removeDashboard(this)', 'resetDashboard(this)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, '!IsNoviceMode()'];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts const childContextMenuLabels = ["Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters @@ -605,7 +541,7 @@ pie title Minerals in my tap water /// initializes the left sidebar File system pane static setupFilesystem(doc: Doc, field:string) { - var myFilesystem = DocCast(doc[field]); + const myFilesystem = DocCast(doc[field]); const newFolderOpts: DocumentOptions = { _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), @@ -650,7 +586,7 @@ pie title Minerals in my tap water /// initializes the left sidebar panel view of the UserDoc static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { - _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", + _lockedPosition: true, _gridGap: 5, _forceActive: true, title: ClientUtils.CurrentUserEmail +"-view", layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, ignoreClick: true, isSystem: true, treeView_HideTitle: true, treeView_TruncateTitleWidth: 350 }; @@ -671,7 +607,7 @@ pie title Minerals in my tap water static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ btnType: ButtonType.ToolButton, _dropPropertiesToRemove: new List<string>([ "layout_hideContextMenu"]), - /*_nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts, + /* _nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts, }) /// initializes the required buttons in the expanding button menu at the bottom of the Dash window @@ -694,8 +630,8 @@ pie title Minerals in my tap water title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move, childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true }; - reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); - reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + reaction(() => UndoManager.redoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4; }, { fireImmediately: true }); + reaction(() => UndoManager.undoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4; }, { fireImmediately: true }); return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } @@ -752,9 +688,9 @@ pie title Minerals in my tap water { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, - { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.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(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.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(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, + { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, { title: "Labels", toolTip: "Lab els", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, }, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1}, @@ -835,13 +771,13 @@ pie title Minerals in my tap water static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc { const menuBtnDoc = DocListCast(menuDoc?.data).find( doc => doc.title === params.title); - const subMenu = params.subMenu; + const {subMenu} = params; if (!subMenu) { // button does not have a sub menu return this.setupContextMenuButton(params, menuBtnDoc, menuDoc); } // linear view const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]), - childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, + childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick, linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc}; const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) ); @@ -873,10 +809,10 @@ pie title Minerals in my tap water { opts: { title: "Replicate",icon:"camera",toolTip: "Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true}, scripts: { onClick: `snapshotDashboard()`}}, { opts: { title: "Recordings", toolTip: "Workspace Recordings", btnType: ButtonType.DropdownList,expertMode: false, ignoreClick: true, width: 100}, funcs: {hidden: `false`, btnList:`getWorkspaceRecordings()`}, scripts: { script: `{ return replayWorkspace(value, _readOnly_); }`, onDragScript: `{ return startRecordingDrag(value); }`}}, { opts: { title: "Stop Rec",icon: "stop", toolTip: "Stop recording", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `!isWorkspaceRecording()`}, scripts: { onClick: `stopWorkspaceRecording()`}}, - { opts: { title: "Play", icon: "play", toolTip: "Play recording", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}}, - { opts: { title: "Pause", icon: "pause",toolTip: "Pause playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Playing}"`}, scripts: { onClick: `pauseWorkspaceReplaying(getCurrentRecording())`}}, - { opts: { title: "Stop", icon: "stop", toolTip: "Stop playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `stopWorkspaceReplaying(getCurrentRecording())`}}, - { opts: { title: "Delete", icon: "trash",toolTip: "delete selected rec", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `removeWorkspaceReplaying(getCurrentRecording())`}} + { opts: { title: "Play", icon: "play", toolTip: "Play recording", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`}, scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}}, + { opts: { title: "Pause", icon: "pause",toolTip: "Pause playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Playing}"`}, scripts: { onClick: `pauseWorkspaceReplaying(getCurrentRecording())`}}, + { opts: { title: "Stop", icon: "stop", toolTip: "Stop playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`}, scripts: { onClick: `stopWorkspaceReplaying(getCurrentRecording())`}}, + { opts: { title: "Delete", icon: "trash",toolTip: "delete selected rec", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`}, scripts: { onClick: `removeWorkspaceReplaying(getCurrentRecording())`}} ]; const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs)); const dockBtnsReqdOpts:DocumentOptions = { @@ -890,12 +826,14 @@ pie title Minerals in my tap water return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true }); } - /// The database of all links on all documents + static newAccount: boolean = false; + + // The database of all links on all documents static setupLinkDocs(doc: Doc, linkDatabaseId: string) { - if (!(Docs.newAccount ? undefined : DocCast(doc.myLinkDatabase))) { + if (!(CurrentUserUtils.newAccount ? undefined : DocCast(doc.myLinkDatabase))) { const linkDocs = new Doc(linkDatabaseId, true); - linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail; - linkDocs.author = Doc.CurrentUserEmail; + linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail; + linkDocs.author = ClientUtils.CurrentUserEmail; linkDocs.isSystem = true; linkDocs.data = new List<Doc>([]); linkDocs["acl-Guest"] = SharingPermissions.Augment; @@ -949,16 +887,17 @@ pie title Minerals in my tap water /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field /// whether to revert to "default" values, or to leave them as the user/system last set them. - static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + static updateUserDocument(docIn: Doc, sharingDocumentId: string, linkDatabaseId: string) { + const doc = docIn; DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); - reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data_modificationDate"]), + reaction(() => DateCast(DocCast(doc.globalGroupDatabase).data_modificationDate), async () => { const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); - const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; - SetCachedGroups(["Guest", ...mygroups?.map(g => StrCast(g.title))]); + const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(ClientUtils.CurrentUserEmail)) || []; + SetCachedGroups(["Guest", ...(mygroups?.map(g => StrCast(g.title))??[])]); }, { fireImmediately: true }); doc.isSystem ?? (doc.isSystem = true); - doc.title ?? (doc.title = Doc.CurrentUserEmail); + doc.title ?? (doc.title = ClientUtils.CurrentUserEmail); Doc.noviceMode ?? (Doc.noviceMode = true); doc._showLabel ?? (doc._showLabel = true); doc.textAlign ?? (doc.textAlign = "left"); @@ -971,7 +910,7 @@ pie title Minerals in my tap water doc.activeFillColor ?? (doc.activeFillColor = ""); doc.activeArrowStart ?? (doc.activeArrowStart = ""); doc.activeArrowEnd ?? (doc.activeArrowEnd = ""); - doc.activeDash ?? (doc.activeDash == "0"); + doc.activeDash ?? (doc.activeDash === "0"); doc.fontSize ?? (doc.fontSize = "12px"); doc.fontFamily ?? (doc.fontFamily = "Arial"); doc.fontColor ?? (doc.fontColor = "black"); @@ -988,15 +927,14 @@ pie title Minerals in my tap water this.setupLinkDocs(doc, linkDatabaseId); this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon - this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile this.setupPublished(doc); // sets up the list doc of all docs that have been published (meaning that they can be auto-linked by typing their title into another text box) this.setupContextMenuButtons(doc); // set up the row of buttons at the top of the dashboard that change depending on what is selected this.setupTopbarButtons(doc); this.setupDockedButtons(doc); // the bottom bar of font icons this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left this.setupDocTemplates(doc); // sets up the template menu of templates - //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption - DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); + // sthis.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption + DocUtils.AssignDocField(doc, "globalScriptDatabase", () => Docs.Prototypes.MainScriptDocument(), {}); DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, layout_maxShown: 10, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: dropActionType.move}); // drop down panel at top of dashboard for stashing documents Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards) @@ -1005,10 +943,11 @@ pie title Minerals in my tap water Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org") + // eslint-disable-next-line no-new new LinkManager(); - DocServer.CacheNeedsUpdate && setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500); - setInterval(DocServer.UPDATE_SERVER_CACHE, 120000); + DocServer.CacheNeedsUpdate && setTimeout(UPDATE_SERVER_CACHE, 2500); + setInterval(UPDATE_SERVER_CACHE, 120000); return doc; } static setupFieldInfos(doc:Doc, field="fieldInfos") { @@ -1032,11 +971,11 @@ pie title Minerals in my tap water @observable public static ServerVersion: string = ';' public static async loadCurrentUser() { - return rp.get(Utils.prepend("/getCurrentUser")).then(async response => { + return rp.get(ClientUtils.prepend("/getCurrentUser")).then(async response => { if (response) { const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string, email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response); - runInAction(() => CurrentUserUtils.ServerVersion = result.version); - Doc.CurrentUserEmail = result.email; + runInAction(() => { CurrentUserUtils.ServerVersion = result.version; }); + ClientUtils.CurrentUserEmail = result.email; resolvedPorts = result.resolvedPorts as any; DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email); if (result.cacheDocumentIds) @@ -1044,13 +983,13 @@ pie title Minerals in my tap water const ids = result.cacheDocumentIds.split(";"); const batch = 30000; for (let i = 0; i < ids.length; i += batch) { + // eslint-disable-next-line no-await-in-loop await DocServer.GetRefFields(ids.slice(i, i+batch)); } } return result; - } else { - throw new Error("There should be a user! Why does Dash think there isn't one?"); - } + } + throw new Error("There should be a user! Why does Dash think there isn't one?"); }); } @@ -1060,12 +999,12 @@ pie title Minerals in my tap water linkDatabaseId: string; }) { return DocServer.GetRefField(info.userDocumentId).then(async field => { - Docs.newAccount = !(field instanceof Doc); + CurrentUserUtils.newAccount = !(field instanceof Doc); await Docs.Prototypes.initialize(); - const userDoc = Docs.newAccount ? new Doc(info.userDocumentId, true) : field as Doc; + const userDoc = CurrentUserUtils.newAccount ? new Doc(info.userDocumentId, true) : field as Doc; this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId); - if (Docs.newAccount) { - if (Doc.CurrentUserEmail === "guest") { + if (CurrentUserUtils.newAccount) { + if (ClientUtils.CurrentUserEmail === "guest") { DashboardView.createNewDashboard(undefined, "guest dashboard"); } else { userDoc.activePage = "home"; @@ -1082,14 +1021,10 @@ pie title Minerals in my tap water input.type = "file"; input.multiple = true; input.accept = ".zip, application/pdf, video/*, image/*, audio/*"; - input.onchange = async _e => { + input.onchange = async () => { const file = input.files?.[0]; if (file?.type === 'application/zip' || file?.type === 'application/x-zip-compressed') { const doc = await Doc.importDocument(file); - // NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) { - // setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => - // docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. - // } const list = Cast(Doc.MyImports.data, listSpec(Doc), null); doc instanceof Doc && list?.splice(0, 0, doc); } else if (input.files && input.files.length !== 0) { @@ -1109,10 +1044,17 @@ pie title Minerals in my tap water } } -ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); -ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode"); -ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode"); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); -ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); -ScriptingGlobals.add(function getSharingDoc() {return Doc.SharingDoc() });
\ No newline at end of file +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() });
\ No newline at end of file diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 82c63695c..207d3ea0b 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -1,15 +1,15 @@ import * as interpreter from 'words-to-numbers'; // @ts-ignore bcz: how are you supposed to include these definitions since dom-speech-recognition isn't a module? import type {} from '@types/dom-speech-recognition'; +import { ClientUtils } from '../../ClientUtils'; import { Doc, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { listSpec } from '../../fields/Schema'; import { Cast, CastCtor, DocCast } from '../../fields/Types'; import { AudioField, ImageField } from '../../fields/URLField'; -import { Utils } from '../../Utils'; -import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; +import { Docs } from '../documents/Documents'; import { DictationOverlay } from '../views/DictationOverlay'; import { DocumentView, OpenWhere } from '../views/nodes/DocumentView'; import { SelectionManager } from './SelectionManager'; @@ -103,7 +103,7 @@ export namespace DictationManager { results = await (pendingListen = listenImpl(options)); pendingListen = undefined; if (results) { - Utils.CopyText(results); + ClientUtils.CopyText(results); if (overlay) { DictationOverlay.Instance.isListening = false; const execute = options?.tryExecute; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 40d28c690..67a61f17e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -8,10 +8,9 @@ import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { AudioField } from '../../fields/URLField'; import { GetEffectiveAcl } from '../../fields/util'; import { CollectionViewType } from '../documents/DocumentTypes'; -import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { TabDocView } from '../views/collections/TabDocView'; import { LightboxView } from '../views/LightboxView'; -import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, OpenWhere } from '../views/nodes/DocumentView'; import { FocusViewOptions } from '../views/nodes/FieldView'; import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; @@ -20,12 +19,14 @@ import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; export class DocumentManager { + // eslint-disable-next-line no-use-before-define private static _instance: DocumentManager; public static get Instance(): DocumentManager { + // eslint-disable-next-line no-return-assign return this._instance || (this._instance = new this()); } - //global holds all of the nodes (regardless of which collection they're in) + // global holds all of the nodes (regardless of which collection they're in) @observable _documentViews = new Set<DocumentView>(); @observable.shallow public CurrentlyLoading: Doc[] = []; @computed public get DocumentViews() { @@ -38,7 +39,7 @@ export class DocumentManager { this._documentViews.delete(dv); } - //private constructor so no other class can create a nodemanager + // private constructor so no other class can create a nodemanager private constructor() { makeObservable(this); observe(this.CurrentlyLoading, change => { @@ -52,6 +53,7 @@ export class DocumentManager { case 'splice': (change as any).removed.forEach((doc: Doc) => DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => StrCast(dv.Document.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()))); break; + default: } }); } @@ -101,7 +103,7 @@ export class DocumentManager { SelectionManager.DeselectView(view); }); - //gets all views + // gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; DocumentManager.Instance.DocumentViews.forEach(view => { @@ -135,25 +137,17 @@ export class DocumentManager { ); } - public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { + public getLightboxDocumentView = (toFind: Doc): DocumentView | undefined => { const views: DocumentView[] = []; DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.Contains(view) && Doc.AreProtosEqual(view.Document, 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 => { - if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc); - const views = this.getDocumentViews(toFind); //.filter(view => view.Document !== 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 getFirstDocumentView = (toFind: Doc): DocumentView | undefined => { + if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind); + const views = this.getDocumentViews(toFind); // .filter(view => view.Document !== 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(toFindIn: Doc): DocumentView[] { - const toFind = - // Array.from(DocumentManager.Instance.DocumentViews).find( - // dv => - // ((dv.Document.data as any)?.url?.href && (dv.Document.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) || - // ((DocCast(dv.Document.annotationOn)?.data as any)?.url?.href && (DocCast(dv.Document.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href) - // )?.Document ?? - toFindIn; - + public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.Contains(view)); const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.Contains(view)); @@ -172,7 +166,7 @@ export class DocumentManager { static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) { if (!doc) return []; const srcContext = DocCast(doc.annotationOn, DocCast(doc.embedContainer)); - var containerDocContext = srcContext ? [srcContext, doc] : [doc]; + let containerDocContext = srcContext ? [srcContext, doc] : [doc]; while ( containerDocContext.length && DocCast(containerDocContext[0]?.embedContainer) && @@ -204,10 +198,6 @@ export class DocumentManager { DocumentManager._overlayViews?.clear(); } static _overlayViews = new ObservableSet<DocumentView>(); - static addView = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddSplit(doc, OpenWhereMod.right); - finished?.(); - }; public static LinkCommonAncestor(linkDoc: Doc) { const anchor = (which: number) => { @@ -228,7 +218,7 @@ export class DocumentManager { // focusing on each context public showDocumentView = async (targetDocView: DocumentView, options: FocusViewOptions) => { const docViewPath = [...(targetDocView.containerViewPath?.() ?? []), targetDocView]; - let rootContextView = docViewPath.shift(); + const rootContextView = docViewPath.shift(); await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined, focused: false }))); if (options.toggleTarget && (!options.didMove || targetDocView.Document.hidden)) targetDocView.Document.hidden = !targetDocView.Document.hidden; else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.Document, options.openLocation); @@ -242,9 +232,10 @@ export class DocumentManager { // 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 - options: FocusViewOptions, // options for how to navigate to target + optionsIn: FocusViewOptions, // options for how to navigate to target finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. ) => { + const options = optionsIn; Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; @@ -253,13 +244,14 @@ export class DocumentManager { options.toggleTarget = false; TabDocView.Activate(tabView?._document); } - let rootContextView = + const rootContextView = docContextPath.length && (await new Promise<DocumentView>(res => { const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc)); if (viewIndex !== -1) { viewIndex && docContextPath.splice(0, viewIndex); - return res(this.getDocumentView(docContextPath[0])!); + res(this.getDocumentView(docContextPath[0])!); + return; } options.didMove = true; (!LightboxView.LightboxDoc && docContextPath.some(doc => TabDocView.Activate(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); @@ -270,7 +262,9 @@ export class DocumentManager { const target = DocCast(targetDoc.annotationOn, targetDoc); const contextView = this.getDocumentView(DocCast(target.embedContainer)); if (contextView?.ComponentView?.addDocTab?.(target, OpenWhere.lightbox)) { - await new Promise<void>(waitres => setTimeout(() => waitres())); + await new Promise<void>(waitres => { + setTimeout(() => waitres()); + }); } } docContextPath.shift(); @@ -287,19 +281,25 @@ export class DocumentManager { }; focusViewsInPath = async ( - docView: DocumentView, // - options: FocusViewOptions, + docViewIn: DocumentView, // + optionsIn: FocusViewOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean }> ) => { let contextView: DocumentView | undefined; // view containing context that contains target let focused = false; + let docView = docViewIn; + const options = optionsIn; while (true) { if (docView.Document.layout_fieldKey === 'layout_icon') { - await new Promise<void>(res => docView.iconify(res)); + // eslint-disable-next-line no-await-in-loop + await new Promise<any>(res => { + docView.iconify(res); + }); options.didMove = true; } const nextFocus = docView._props.focus(docView.Document, options); // focus the view within its container - focused = focused || (nextFocus === undefined ? false : true); // keep track of whether focusing on a view needed to actually change anything + focused = focused || nextFocus !== undefined; // keep track of whether focusing on a view needed to actually change anything + // eslint-disable-next-line no-await-in-loop const { childDocView, viewSpec } = await iterator(docView); if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.Document, docView, contextView, focused }; contextView = options.anchorDoc?.layout_unrendered && !childDocView.Document.layout_unrendered ? childDocView : docView; @@ -308,10 +308,11 @@ export class DocumentManager { }; @action - restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: FocusViewOptions, contextView: Opt<DocumentView>, targetDoc: Doc) { + restoreDocView(viewSpec: Opt<Doc>, docViewIn: DocumentView, options: FocusViewOptions, contextView: Opt<DocumentView>, targetDoc: Doc) { + const docView = docViewIn; if (viewSpec && docView) { - //if (docView.ComponentView instanceof FormattedTextBox) - //viewSpec !== docView.Document && + // if (docView.ComponentView instanceof FormattedTextBox) + // viewSpec !== docView.Document && docView.ComponentView?.focus?.(viewSpec, options); PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500); Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect); @@ -332,7 +333,10 @@ export class DocumentManager { } } } -export function DocFocusOrOpen(doc: Doc, options: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { +// eslint-disable-next-line default-param-last +export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { + let doc = docIn; + const options = optionsIn; const func = () => { const cv = DocumentManager.Instance.getDocumentView(containingDoc); const dv = DocumentManager.Instance.getDocumentView(doc, cv); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 9627c5df2..62f055f1a 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-mutable-exports */ +/* eslint-disable no-use-before-define */ /** * The DragManager handles all dragging interactions that occur entirely within Dash (as opposed to external drag operations from the file system, etc) * @@ -13,32 +15,25 @@ */ import { action, observable, runInAction } from 'mobx'; +import { ClientUtils } from '../../ClientUtils'; +import { emptyFunction } from '../../Utils'; import { DateField } from '../../fields/DateField'; -import { Doc, Field, Opt, StrListCast } from '../../fields/Doc'; +import { Doc, FieldType, Opt, StrListCast } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { ScriptField } from '../../fields/ScriptField'; import { ScriptCast } from '../../fields/Types'; -import { emptyFunction, Utils } from '../../Utils'; -import { Docs, DocUtils } from '../documents/Documents'; +import { DocUtils, Docs } from '../documents/Documents'; import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../views/nodes/DocumentView'; +import { dropActionType } from './DropActionTypes'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; -import { DocData } from '../../fields/DocSymbols'; -const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore -export enum dropActionType { - embed = 'embed', // create a new embedding of the dragged document for the new location - copy = 'copy', // copy the dragged document - move = 'move', // move the dragged document to the drop location after removing it from where it was - add = 'add', // add the dragged document to the drop location without removing it from where it was - same = 'same', // only allow drop within same collection (or same hierarchical tree collection) - inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier) - proto = 'proto', -} // undefined = move, same = move but doesn't call dropPropertiesToRemove +const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore /** * Initialize drag @@ -83,6 +78,9 @@ export namespace DragManager { let dragLabel: HTMLDivElement; export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => boolean>; export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; + export let AbortDrag: () => void = emptyFunction; + export const docsBeingDragged: Doc[] = observable([]); + export let DocDragData: DocumentDragData | undefined; export function Root() { const root = document.getElementById('root'); @@ -91,7 +89,6 @@ export namespace DragManager { } return root; } - export let AbortDrag: () => void = emptyFunction; export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; export type RemoveFunction = (document: Doc | Doc[]) => boolean; @@ -106,6 +103,7 @@ export namespace DragManager { // event called when the drag operation results in a drop action export class DropEvent { + // eslint-disable-next-line no-useless-constructor constructor( readonly x: number, readonly y: number, @@ -115,7 +113,9 @@ export namespace DragManager { readonly metaKey: boolean, readonly ctrlKey: boolean, readonly embedKey: boolean - ) {} + ) { + /* empty */ + } } // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated @@ -194,7 +194,7 @@ export namespace DragManager { userDropAction?: dropActionType; } - let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + const defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { if (de.complete.docDragData) { targetAction && (de.complete.docDragData.dropAction = targetAction); e.stopPropagation(); @@ -228,7 +228,7 @@ export namespace DragManager { return dropDoc; }; const finishDrag = async (e: DragCompleteEvent) => { - const docDragData = e.docDragData; + const { docDragData } = e; setTimeout(() => dragData.draggedViews.forEach(view => view.props.dragEnding?.())); onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { @@ -256,7 +256,9 @@ export namespace DragManager { .forEach((drop: Doc, i: number) => { const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove); const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps)); - [...remProps, 'dropPropertiesToRemove'].map(prop => (drop[prop] = undefined)); + [...remProps, 'dropPropertiesToRemove'].forEach(prop => { + drop[prop] = undefined; + }); }); } return e; @@ -268,15 +270,18 @@ export namespace DragManager { } // drag a button template and drop a new button - export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { + export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: FieldType }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { const finishDrag = (e: DragCompleteEvent) => { const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }); - params.map(p => Object.keys(vars).indexOf(p) !== -1 && (bd[DocData][p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields + params.forEach(p => { + Object.keys(vars).indexOf(p) !== -1 && (bd[DocData][p] = new PrefetchProxy(vars[p] as Doc)); + }); // copy all "captured" arguments into document parameterfields initialize?.(bd); bd[DocData]['onClick-paramFieldKeys'] = new List<string>(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); return e; }; + // eslint-disable-next-line no-param-reassign options = options ?? {}; options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); @@ -298,7 +303,7 @@ export namespace DragManager { } export function snapDragAspect(dragPt: number[], snapAspect: number) { - let closest = Utils.SNAP_THRESHOLD; + let closest = ClientUtils.SNAP_THRESHOLD; let near = dragPt; const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => { if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0 @@ -307,7 +312,7 @@ export namespace DragManager { const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; // let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; - //if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return undefined; // is the intersection along the segments + // if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return undefined; // is the intersection along the segments // Return a object with the x and y coordinates of the intersection const x = x1 + ua * (x2 - x1); @@ -315,14 +320,14 @@ export namespace DragManager { const dist = Math.sqrt((dragx - x) * (dragx - x) + (dragy - y) * (dragy - y)); return { pt: [x, y], dist }; }; - SnappingManager.VertSnapLines.forEach((xCoord, i) => { + SnappingManager.VertSnapLines.forEach(xCoord => { const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, xCoord, -1, xCoord, 1, dragPt[0], dragPt[1]); if (pt && pt.dist < closest) { closest = pt.dist; near = pt.pt; } }); - SnappingManager.HorizSnapLines.forEach((yCoord, i) => { + SnappingManager.HorizSnapLines.forEach(yCoord => { const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, -1, yCoord, 1, yCoord, dragPt[0], dragPt[1]); if (pt && pt.dist < closest) { closest = pt.dist; @@ -333,7 +338,7 @@ export namespace DragManager { } // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { - const snapThreshold = Utils.SNAP_THRESHOLD; + const snapThreshold = ClientUtils.SNAP_THRESHOLD; const snapVal = (pts: number[], drag: number, snapLines: number[]) => { if (snapLines.length) { const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt @@ -350,14 +355,33 @@ export namespace DragManager { y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.HorizSnapLines), }; } - export let docsBeingDragged: Doc[] = observable([]); - export let CanEmbed = false; - export let DocDragData: DocumentDragData | undefined; - export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) { + + async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { + const dropArgs = { + cancelable: true, // allows preventDefault() to be called to cancel the drop + bubbles: true, + detail: { + ...pos, + complete, + shiftKey: e.shiftKey, + altKey: e.altKey, + metaKey: e.metaKey, + ctrlKey: e.ctrlKey, + embedKey: SnappingManager.CanEmbed, + }, + }; + target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); + UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes) + await finishDrag?.(complete); + UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called + options?.dragComplete?.(complete); + endDrag?.(); + } + export function StartDrag(elesIn: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) { if (dragData.dropAction === 'none' || SnappingManager.ExploreMode) return; DocDragData = dragData as DocumentDragData; const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); - eles = eles.filter(e => e); + const eles = elesIn.filter(e => e); SnappingManager.SetCanEmbed(dragData.canEmbed || false); if (!dragDiv) { dragDiv = document.createElement('div'); @@ -375,9 +399,9 @@ export namespace DragManager { } Object.assign(dragDiv.style, { width: '', height: '', overflow: '' }); dragDiv.hidden = false; - const scalings: number[] = [], - xs: number[] = [], - ys: number[] = []; + const scalings: number[] = []; + const xs: number[] = []; + const ys: number[] = []; const elesCont = { left: Number.MAX_SAFE_INTEGER, @@ -385,7 +409,7 @@ export namespace DragManager { top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER, }; - let rot: number[] = []; + const rot: number[] = []; const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element @@ -471,7 +495,9 @@ export namespace DragManager { .filter(pb => pb.width && pb.height) .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } - [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none')); + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => { + (ele as any).style && ((ele as any).style.pointerEvents = 'none'); + }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { @@ -493,7 +519,11 @@ export namespace DragManager { const hideDragShowOriginalElements = (hide: boolean) => { dragLabel.style.display = hide && !SnappingManager.CanEmbed ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); + setTimeout(() => + eles.forEach(ele => { + ele.hidden = hide; + }) + ); }; options?.hideSource && hideDragShowOriginalElements(true); @@ -505,22 +535,7 @@ export namespace DragManager { const yFromBottom = elesCont.bottom - downY; let scrollAwaiter: Opt<NodeJS.Timeout>; - AbortDrag = () => { - options?.dragComplete?.(new DragCompleteEvent(true, dragData)); - cleanupDrag(true); - }; - - const cleanupDrag = action((undo: boolean) => { - (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.dragEnding?.()); - hideDragShowOriginalElements(false); - document.removeEventListener('pointermove', moveHandler, true); - document.removeEventListener('pointerup', upHandler, true); - SnappingManager.SetIsDragging(false); - if (batch.end() && undo) UndoManager.Undo(); - docsBeingDragged.length = 0; - SnappingManager.SetCanEmbed(false); - }); - var startWindowDragTimer: any; + let startWindowDragTimer: any; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { @@ -580,10 +595,10 @@ export namespace DragManager { defaultPrevented: true, eventPhase: e.eventPhase, isTrusted: true, - preventDefault: () => ('not implemented for this event' ? false : false), - isDefaultPrevented: () => ('not implemented for this event' ? false : false), - stopPropagation: () => ('not implemented for this event' ? false : false), - isPropagationStopped: () => ('not implemented for this event' ? false : false), + preventDefault: () => 'not implemented for this event' && false, + isDefaultPrevented: () => 'not implemented for this event' && false, + stopPropagation: () => 'not implemented for this event' && false, + isPropagationStopped: () => 'not implemented for this event' && false, persist: emptyFunction, timeStamp: e.timeStamp, type: 'dashDragMovePause', @@ -602,7 +617,9 @@ export namespace DragManager { const moveVec = { x: x - lastPt.x, y: y - lastPt.y }; lastPt = { x, y }; - dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg) scale(${scalings[i]})`)); + dragElements.forEach((dragElement, i) => { + dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg) scale(${scalings[i]})`; + }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; }; const upHandler = (e: PointerEvent) => { @@ -610,36 +627,32 @@ export namespace DragManager { startWindowDragTimer = undefined; dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false)); }; + const cleanupDrag = action((undo: boolean) => { + (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.dragEnding?.()); + hideDragShowOriginalElements(false); + document.removeEventListener('pointermove', moveHandler, true); + document.removeEventListener('pointerup', upHandler, true); + SnappingManager.SetIsDragging(false); + if (batch.end() && undo) UndoManager.Undo(); + docsBeingDragged.length = 0; + SnappingManager.SetCanEmbed(false); + }); + AbortDrag = () => { + options?.dragComplete?.(new DragCompleteEvent(true, dragData)); + cleanupDrag(true); + }; document.addEventListener('pointermove', moveHandler, true); document.addEventListener('pointerup', upHandler, true); } - - async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { - const dropArgs = { - cancelable: true, // allows preventDefault() to be called to cancel the drop - bubbles: true, - detail: { - ...pos, - complete, - shiftKey: e.shiftKey, - altKey: e.altKey, - metaKey: e.metaKey, - ctrlKey: e.ctrlKey, - embedKey: SnappingManager.CanEmbed, - }, - }; - target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); - UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes) - await finishDrag?.(complete); - UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called - options?.dragComplete?.(complete); - endDrag?.(); - } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) { if (readOnly) { return SelectionManager.Views.some(dv => dv.Document.keepZWhenDragged); } - SelectionManager.Views.map(dv => (dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged)); + SelectionManager.Views.forEach(dv => { + dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged; + }); + return undefined; }); diff --git a/src/client/util/DropActionTypes.ts b/src/client/util/DropActionTypes.ts new file mode 100644 index 000000000..45b294d97 --- /dev/null +++ b/src/client/util/DropActionTypes.ts @@ -0,0 +1,9 @@ +export enum dropActionType { + embed = 'embed', // create a new embedding of the dragged document for the new location + copy = 'copy', // copy the dragged document + move = 'move', // move the dragged document to the drop location after removing it from where it was + add = 'add', // add the dragged document to the drop location without removing it from where it was + same = 'same', // only allow drop within same collection (or same hierarchical tree collection) + inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier) + proto = 'proto', +} // undefined = move, same = move but doesn't call dropPropertiesToRemove diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index f4f879208..6b20b885b 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -5,19 +5,20 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; +import { ClientUtils } from '../../ClientUtils'; +import { Utils } from '../../Utils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; import { Cast, StrCast } from '../../fields/Types'; -import { Utils } from '../../Utils'; import { MainViewModal } from '../views/MainViewModal'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './GroupManager.scss'; import { GroupMemberView } from './GroupMemberView'; import { SettingsManager } from './SettingsManager'; import { SharingManager, User } from './SharingManager'; -import { ObservableReactComponent } from '../views/ObservableReactComponent'; /** * Interface for options for the react-select component @@ -55,7 +56,7 @@ export class GroupManager extends ObservableReactComponent<{}> { */ populateUsers = async () => { if (Doc.UserDoc()[Id] !== Utils.GuestID()) { - const userList = await RequestPromise.get(Utils.prepend('/getUsers')); + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); const raw = JSON.parse(userList) as User[]; raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); } @@ -135,7 +136,7 @@ export class GroupManager extends ObservableReactComponent<{}> { hasEditAccess(groupDoc: Doc): boolean { if (!groupDoc) return false; const accessList: string[] = JSON.parse(StrCast(groupDoc.owners)); - return accessList.includes(Doc.CurrentUserEmail) || this.adminGroupMembers?.includes(Doc.CurrentUserEmail); + return accessList.includes(ClientUtils.CurrentUserEmail) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail); } /** @@ -147,7 +148,7 @@ export class GroupManager extends ObservableReactComponent<{}> { const name = groupName.toLowerCase() === 'admin' ? 'Admin' : groupName; const groupDoc = new Doc('GROUP:' + name, true); groupDoc.title = name; - groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); + groupDoc.owners = JSON.stringify([ClientUtils.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); this.addGroup(groupDoc); } @@ -176,11 +177,11 @@ export class GroupManager extends ObservableReactComponent<{}> { Doc.RemoveDocFromList(this.GroupManagerDoc, 'data', group); SharingManager.Instance.removeGroup(group); const members = JSON.parse(StrCast(group.members)); - if (members.includes(Doc.CurrentUserEmail)) { + if (members.includes(ClientUtils.CurrentUserEmail)) { const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } - this.GroupManagerDoc['data_modificationDate'] = new DateField(); + this.GroupManagerDoc.data_modificationDate = new DateField(); if (group === this.currentGroup) { this.currentGroup = undefined; } @@ -260,7 +261,10 @@ export class GroupManager extends ObservableReactComponent<{}> { alert('Please select a unique group name'); return; } - this.createGroupDoc(value, this.selectedUsers?.map(user => user.value)); + this.createGroupDoc( + value, + this.selectedUsers?.map(user => user.value) + ); this.selectedUsers = null; this.inputRef.current!.value = ''; this.buttonColour = '#979797'; diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 2f1a336cc..b500a5af9 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -1,6 +1,6 @@ import * as qs from 'query-string'; import { Doc } from '../../fields/Doc'; -import { OmitKeys, Utils } from '../../Utils'; +import { OmitKeys, ClientUtils } from '../../ClientUtils'; import { DocServer } from '../DocServer'; import { DashboardView } from '../views/DashboardView'; @@ -136,7 +136,7 @@ export namespace HistoryUtil { function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) { stringifiers[type] = state => { - let path = Utils.prepend(`/${type}`); + let path = ClientUtils.prepend(`/${type}`); if (customStringifier) { path = customStringifier(state, path); } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index c5f307f44..d1600468a 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -1,5 +1,5 @@ import { action, runInAction } from 'mobx'; -import { simulateMouseClick } from '../../Utils'; +import { simulateMouseClick } from '../../ClientUtils'; import { Doc, Opt } from '../../fields/Doc'; import { Cast, StrCast } from '../../fields/Types'; import { WebField } from '../../fields/URLField'; @@ -33,7 +33,7 @@ export namespace Hypothesis { // 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); + // const filteredDocs = docs.filter(doc => doc.author === ClientUtils.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? // }); diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index d99828956..dfad9755c 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,10 +1,10 @@ +import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { Cast, StrCast, NumCast } from '../../../fields/Types'; import { Networking } from '../../Network'; -import { Id } from '../../../fields/FieldSymbols'; -import { Utils } from '../../../Utils'; -import { DocData } from '../../../fields/DocSymbols'; export namespace ImageUtils { export type imgInfo = { @@ -35,7 +35,7 @@ export namespace ImageUtils { export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => { const a = document.createElement('a'); - a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`); + a.href = ClientUtils.prepend(`/imageHierarchyExport/${collection[Id]}`); a.download = `Dash Export [${StrCast(collection.title)}].zip`; a.click(); }; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index a2f5826fe..a07550e09 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { GestureUtils } from '../../pen-gestures/GestureUtils'; import { Utils } from '../../Utils'; +import { Gestures } from '../../pen-gestures/GestureTypes'; import './InteractionUtils.scss'; export namespace InteractionUtils { @@ -9,81 +9,86 @@ export namespace InteractionUtils { export const PENTYPE = 'pen'; export const ERASERTYPE = 'eraser'; - const POINTER_PEN_BUTTON = -1; - const REACT_POINTER_PEN_BUTTON = 0; const ERASER_BUTTON = 5; - export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> { - constructor( - readonly fingers: number, - readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[], - 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; - } - - /** - * - * @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 { - 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); - // if (onMultiTouchMoveHandler) { - // element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler); - // } - // if (onMultiTouchEndHandler) { - // element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler); - // } - return () => { - element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler); - // if (onMultiTouchMoveHandler) { - // element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler); - // } - // if (onMultiTouchEndHandler) { - // element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler); - // } - }; - } - - /** - * Turns an element onto a target for touch hold handling. - * @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 { - const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail); - element.addEventListener('dashOnTouchHoldStart', handler); - return () => { - element.removeEventListener('dashOnTouchHoldStart', handler); - }; - } - - export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] { - const myTouches = new Array<React.Touch>(); - for (const pt of mte.touches) { - if (!ignorePen || ((pt as any).radiusX > 1 && (pt as any).radiusY > 1)) { - for (const tPt of mte.targetTouches) { - if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) { - if (pt && prevPoints.has(pt.identifier)) { - myTouches.push(pt); - } - } + export function makePolygon(shape: string, points: { X: number; Y: number }[]) { + // if arrow/line/circle, the two end points should be the starting and the ending point + let left = points[0].X; + let top = points[0].Y; + let right = points[1].X; + let bottom = points[1].Y; + 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 (![Gestures.Arrow, Gestures.Line, Gestures.Circle].includes(shape as any as Gestures)) { + // otherwise take max and min + const xs = points.map(p => p.X); + const ys = points.map(p => p.Y); + right = Math.max(...xs); + left = Math.min(...xs); + bottom = Math.max(...ys); + top = Math.min(...ys); + } + } else { + // if in the middle of drawing + // take first and last points + right = points[points.length - 1].X; + left = points[0].X; + bottom = points[points.length - 1].Y; + top = points[0].Y; + if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) { + // switch left/right and top/bottom if needed + if (left > right) { + const temp = right; + right = left; + left = temp; + } + if (top > bottom) { + const temp = top; + top = bottom; + bottom = temp; } } } - // if (mte.touches.length !== myTouches.length) { - // throw Error("opo") - // } - return myTouches; + const polyPts = []; + switch (shape) { + case Gestures.Rectangle: + polyPts.push({ X: left, Y: top }); + polyPts.push({ X: right, Y: top }); + polyPts.push({ X: right, Y: bottom }); + polyPts.push({ X: left, Y: bottom }); + polyPts.push({ X: left, Y: top }); + break; + case Gestures.Triangle: + polyPts.push({ X: left, Y: bottom }); + polyPts.push({ X: right, Y: bottom }); + polyPts.push({ X: (right + left) / 2, Y: top }); + polyPts.push({ X: left, Y: bottom }); + break; + case 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)); + for (let x = centerX - radius; x < centerX + radius; x++) { + const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY; + polyPts.push({ X: x, Y: y }); + } + for (let x = centerX + radius; x > centerX - radius; x--) { + const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY; + const newY = centerY - (y - centerY); + polyPts.push({ X: x, Y: newY }); + } + polyPts.push({ X: centerX - radius, Y: Math.sqrt(radius ** 2 - (-radius) ** 2) + centerY }); + } + break; + + case Gestures.Line: + default: + polyPts.push({ X: left, Y: top }); + polyPts.push({ X: right, Y: bottom }); + break; + } + return polyPts; } export function CreatePolyline( @@ -101,20 +106,20 @@ export namespace InteractionUtils { arrowEnd: string, markerScale: number, dash: string | undefined, - scalex: number, - scaley: number, + scalexIn: number, + scaleyIn: number, shape: string, pevents: string, opacity: number, nodefs: boolean, downHdlr?: (e: React.PointerEvent) => void, - mask?: boolean, - dropshadow?: string + mask?: boolean + // dropshadow?: string ) { const pts = shape ? makePolygon(shape, points) : points; - if (isNaN(scalex)) scalex = 1; - if (isNaN(scaley)) scaley = 1; + const scalex = isNaN(scalexIn) ? 1 : scalexIn; + const scaley = isNaN(scaleyIn) ? 1 : scaleyIn; 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 @@ -137,7 +142,7 @@ export namespace InteractionUtils { <defs> {!mask ? null : ( <filter id={`mask${defGuid}`} x="-1" y="-1" width="500%" height="500%"> - <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5"></feGaussianBlur> + <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5" /> </filter> )} {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : ( @@ -172,7 +177,7 @@ export namespace InteractionUtils { <Tag d={bezier ? strpts + (arrowStart || arrowEnd ? ' ' : '') : undefined} points={bezier ? undefined : strpts} - //filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `} + // filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `} style={{ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, fill: fill && fill !== 'transparent' ? fill : 'none', @@ -193,83 +198,6 @@ export namespace InteractionUtils { ); } - 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 === 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; - var right = points[1].X; - var bottom = points[1].Y; - } else { - //otherwise take max and min - const xs = points.map(p => p.X); - const ys = points.map(p => p.Y); - right = Math.max(...xs); - left = Math.min(...xs); - bottom = Math.max(...ys); - top = Math.min(...ys); - } - } else { - //if in the middle of drawing - //take first and last points - right = points[points.length - 1].X; - left = points[0].X; - bottom = points[points.length - 1].Y; - top = points[0].Y; - 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; - right = left; - left = temp; - } - if (top > bottom) { - const temp = top; - top = bottom; - bottom = temp; - } - } - } - points = []; - switch (shape) { - 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 }); - break; - case GestureUtils.Gestures.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.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)); - 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 }); - } - 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 }); - break; - } - return points; - } /** * Returns whether or not the pointer event passed in is of the type passed in * @param e - pointer event. this event could be from a mouse, a pen, or a finger @@ -279,10 +207,11 @@ export namespace InteractionUtils { // 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 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; - } + case TOUCHTYPE: return e.pointerType === TOUCHTYPE; + default: + } // prettier-ignore return e.pointerType === type; } @@ -292,7 +221,7 @@ export namespace InteractionUtils { * @param pt2 */ export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number { - return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2)); + return Math.sqrt((pt1.clientX - pt2.clientX) ** 2 + (pt1.clientY - pt2.clientY) ** 2); } /** @@ -349,72 +278,4 @@ export namespace InteractionUtils { } return 0; } - - export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean { - for (const touch of newTouches) { - if (touch) { - const oldTouch = oldTouches.get(touch.identifier); - if (oldTouch) { - if (TwoPointEuclidist(touch, oldTouch) >= leniency) { - return true; - } - } - } - } - return false; - } - - // 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 - * @param pts - List of points - */ - // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } { - // const leniency = 200; - // switch (pts.length) { - // case 1: - // return { type: OneFinger }; - // case 2: - // return { type: TwoSeperateFingers }; - // case 3: - // let pt1 = pts[0]; - // let pt2 = pts[1]; - // let pt3 = pts[2]; - // if (pt1 && pt2 && pt3) { - // let dist12 = TwoPointEuclidist(pt1, pt2); - // let dist23 = TwoPointEuclidist(pt2, pt3); - // let dist13 = TwoPointEuclidist(pt1, pt3); - // let dist12close = dist12 < leniency; - // let dist23close = dist23 < leniency; - // let dist13close = dist13 < leniency; - // let xor2313 = dist23close ? !dist13close : dist13close; - // let xor = dist12close ? !xor2313 : xor2313; - // // three input xor because javascript doesn't have logical xor's - // if (xor) { - // let points: number[] = []; - // let min = Math.min(dist12, dist23, dist13); - // switch (min) { - // case dist12: - // points = [0, 1, 2]; - // break; - // case dist23: - // points = [1, 2, 0]; - // break; - // case dist13: - // points = [0, 2, 1]; - // break; - // } - // return { type: TwoToOneFingers, data: points }; - // } - // else { - // return { type: ThreeSeperateFingers, data: null }; - // } - // } - // default: - // return { type: undefined }; - // } - // } - } } diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 6c0bf3242..ba715aa1f 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -1,5 +1,5 @@ import { action, runInAction } from 'mobx'; -import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, FieldType, FieldResult, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; @@ -138,6 +138,6 @@ ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) { export function FollowLinkScript() { return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' }); } -export function IsFollowLinkScript(field: FieldResult<Field>) { +export function IsFollowLinkScript(field: FieldResult<FieldType>) { return ScriptCast(field)?.script.originalScript.includes('return followLink('); } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 8972bf705..82cd791cc 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,12 +1,16 @@ -import { action, makeObservable, observable, observe, runInAction } from 'mobx'; +import { action, makeObservable, observable, observe } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; +import * as rp from 'request-promise'; +import { ClientUtils } from '../../ClientUtils'; +import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from '../../fields/Doc'; import { DirectLinks, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; +import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; +import { DocumentType } from '../documents/DocumentTypes'; import { ScriptingGlobals } from './ScriptingGlobals'; /* * link doc: @@ -45,6 +49,7 @@ export class LinkManager { constructor() { makeObservable(this); LinkManager._instance = this; + Doc.AddLink = this.addLink; this.createlink_relationshipLists(); // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto @@ -84,7 +89,7 @@ export class LinkManager { ); const watchUserLinkDB = (userLinkDBDoc: Doc) => { - 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 + const toRealField = (field: FieldType) => (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, @@ -148,7 +153,7 @@ export class LinkManager { Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); - setTimeout(DocServer.UPDATE_SERVER_CACHE, 100); + setTimeout(UPDATE_SERVER_CACHE, 100); } } public deleteLink(linkDoc: Doc) { @@ -240,6 +245,42 @@ export class LinkManager { } } +let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server) +export function UPDATE_SERVER_CACHE() { + const prototypes = Object.values(DocumentType) + .filter(type => type !== DocumentType.NONE) + .map(type => DocServer._cache[type + 'Proto']) + .filter(doc => doc instanceof Doc) + .map(doc => doc as Doc); + const references = new Set<Doc>(prototypes); + Doc.FindReferences(Doc.UserDoc(), references, undefined); + DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => { + if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) { + Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link); + Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link); + } + }); + LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined)); + const filtered = Array.from(references); + + const newCacheUpdate = filtered.map(doc => doc[Id]).join(';'); + if (newCacheUpdate === cacheDocumentIds) return; + cacheDocumentIds = newCacheUpdate; + + // print out cached docs + Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = '); + const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc)); + const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); + Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); + + rp.post(ClientUtils.prepend('/setCacheDocumentIds'), { + body: { + cacheDocumentIds, + }, + json: true, + }); +} + ScriptingGlobals.add( function links(doc: any) { return new List(LinkManager.Links(doc)); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 422e708bc..de5e8b92e 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -4,12 +4,14 @@ // // @ts-ignore // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' // @ts-ignore -import * as typescriptlib from '!!raw-loader!./type_decls.d'; +// eslint-disable-next-line node/no-unpublished-import import * as ts from 'typescript'; -import { Doc, Field } from '../../fields/Doc'; +import * as typescriptlib from '!!raw-loader!./type_decls.d'; +import { Doc, FieldType } from '../../fields/Doc'; import { RefField } from '../../fields/RefField'; import { ScriptField } from '../../fields/ScriptField'; -import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; +import { ScriptingGlobals, scriptingGlobals } from './ScriptingGlobals'; + export { ts }; export interface ScriptSuccess { @@ -30,6 +32,7 @@ export type ScriptParam = { [name: string]: string }; export interface CompiledScript { readonly compiled: true; readonly originalScript: string; + // eslint-disable-next-line no-use-before-define readonly options: Readonly<ScriptOptions>; run(args?: { [name: string]: any }, onError?: (res: any) => void, errorVal?: any): ScriptResult; } @@ -47,6 +50,7 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is return false; } +// eslint-disable-next-line no-use-before-define function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors.length) || !script) { @@ -60,6 +64,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an // let params: any[] = [Docs, ...fieldTypes]; const compiledFunction = (() => { try { + // eslint-disable-next-line no-new-func return new Function(...paramNames, `return ${script}`); } catch (e) { console.log(e); @@ -68,20 +73,17 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an })(); if (!compiledFunction) return { compiled: false, errors }; const { capturedVariables = {} } = options; + // eslint-disable-next-line default-param-last const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; + // eslint-disable-next-line no-restricted-syntax for (const name of customParams) { - if (name === 'this') { - continue; - } - if (name in args) { - argsArray.push(args[name]); - } else { - argsArray.push(capturedVariables[name]); + if (name !== 'this') { + argsArray.push(name in args ? args[name] : capturedVariables[name]); } } const thisParam = args.this || capturedVariables.this; - let batch: { end(): void } | undefined = undefined; + let batch: { end(): void } | undefined; try { if (!options.editable) { batch = Doc.MakeReadOnly(); @@ -109,7 +111,7 @@ class ScriptingCompilerHost { files: File[] = []; // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined { - getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined { + getSourceFile(fileName: string, languageVersion: any /* , onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined */): any | undefined { const contents = this.readFile(fileName); if (contents !== undefined) { return ts.createSourceFile(fileName, contents, languageVersion, true); @@ -118,7 +120,7 @@ class ScriptingCompilerHost { } // getDefaultLibFileName(options: ts.CompilerOptions): string { - getDefaultLibFileName(options: any): string { + getDefaultLibFileName(/* options: any */): string { return 'node_modules/typescript/lib/lib.d.ts'; // No idea what this means... } writeFile(fileName: string, content: string) { @@ -157,7 +159,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void; export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { transformer: ts.TransformerFactory<ts.Node>; - getVars?: () => { [name: string]: Field }; + getVars?: () => { [name: string]: FieldType }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value @@ -210,12 +212,13 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const newCaptures = options.transformer.getVars?.(); if (Object.keys(newCaptures ?? {}).length) { // tslint:disable-next-line: prefer-object-spread - //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any; + // options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any; - const transformed = result.transformed; + const { transformed } = result; const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, }); + // eslint-disable-next-line no-param-reassign script = printer.printFile(transformed[0].getSourceFile()); } result.dispose(); @@ -224,19 +227,23 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if ('this' in params || 'this' in capturedVariables) { paramNames.push('this'); } + // eslint-disable-next-line no-restricted-syntax for (const key in params) { - if (key === 'this') continue; - paramNames.push(key); + if (key !== 'this') { + paramNames.push(key); + } } const paramList = paramNames.map(key => { const val = params[key]; return `${key}: ${val}`; }); + // eslint-disable-next-line no-restricted-syntax for (const key in capturedVariables) { - if (key === 'this') continue; - const val = capturedVariables[key]; - paramNames.push(key); - paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`); + if (key !== 'this') { + const val = capturedVariables[key]; + paramNames.push(key); + paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`); + } } const paramString = paramList.join(', '); const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script; @@ -261,6 +268,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp } ScriptingGlobals.add(CompileScript); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function runScript(doc: Doc, script: ScriptField) { return script?.script.run({ this: doc }).result; }); diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts index f151acd81..bc159ed65 100644 --- a/src/client/util/ScriptingGlobals.ts +++ b/src/client/util/ScriptingGlobals.ts @@ -1,8 +1,18 @@ +import ts from 'typescript'; -import * as ts from "typescript"; export { ts }; +const _scriptingGlobals: { [name: string]: any } = {}; +const _scriptingDescriptions: { [name: string]: any } = {}; +const _scriptingParams: { [name: string]: any } = {}; +// eslint-disable-next-line import/no-mutable-exports +export let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; export namespace ScriptingGlobals { + export function getGlobals() { return Object.keys(_scriptingGlobals); } // prettier-ignore + export function getGlobalObj() { return _scriptingGlobals; } // prettier-ignore + export function getDescriptions() { return _scriptingDescriptions; } // prettier-ignore + export function getParameters() { return _scriptingParams; } // prettier-ignore + export function add(global: { name: string }): void; export function add(name: string, global: any): void; export function add(global: { name: string }, decription?: string, params?: string): void; @@ -11,7 +21,7 @@ export namespace ScriptingGlobals { let obj: any; if (second !== undefined) { - if (typeof first === "string") { + if (typeof first === 'string') { n = first; obj = second; } else { @@ -22,18 +32,20 @@ export namespace ScriptingGlobals { _scriptingParams[n] = third; } } - } else if (first && typeof first.name === "string") { + } else if (first && typeof first.name === 'string') { n = first.name; obj = first; } else { - throw new Error("Must either register an object with a name, or give a name and an object"); + throw new Error('Must either register an object with a name, or give a name and an object'); } - if (n === undefined || n === "undefined") { + if (n === undefined || n === 'undefined') { return false; - } else if (_scriptingGlobals.hasOwnProperty(n)) { + } + if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } _scriptingGlobals[n] = obj; + return true; } export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) { return { ..._scriptingGlobals, ...(globals || {}) }; @@ -57,25 +69,16 @@ export namespace ScriptingGlobals { return false; } - export function resetScriptingGlobals() { scriptingGlobals = _scriptingGlobals; } + export function resetScriptingGlobals() { + scriptingGlobals = _scriptingGlobals; + } // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]); - export function printNodeType(node: any, indentation = "") { console.log(indentation + ts.SyntaxKind[node.kind]); } - - export function getGlobals() { return Object.keys(_scriptingGlobals); } - - export function getGlobalObj() { return _scriptingGlobals; } - - export function getDescriptions() { return _scriptingDescriptions; } - - export function getParameters() { return _scriptingParams; } + export function printNodeType(node: any, indentation = '') { + console.log(indentation + ts.SyntaxKind[node.kind]); + } } -export function scriptingGlobal(constructor: { new(...args: any[]): any }) { +export function scriptingGlobal(constructor: { new (...args: any[]): any }) { ScriptingGlobals.add(constructor); } - -const _scriptingGlobals: { [name: string]: any } = {}; -export let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; -const _scriptingDescriptions: { [name: string]: any } = {}; -const _scriptingParams: { [name: string]: any } = {};
\ No newline at end of file diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index fff2737b6..65b9a977d 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -1,5 +1,5 @@ import { ObservableMap } from 'mobx'; -import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; @@ -30,7 +30,7 @@ export namespace SearchUtil { (onlyKeys ?? SearchUtil.documentKeys(doc)).forEach( key => (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))( - matchKeyNames ? key : Field.toString(doc[key] as Field)) + matchKeyNames ? key : Field.toString(doc[key] as FieldType)) && hlights.add(key) ); // prettier-ignore blockedKeys.forEach(key => hlights.delete(key)); @@ -71,7 +71,7 @@ export namespace SearchUtil { docs.filter(d => d && !visited.includes(d)).forEach(d => { visited.push(d); const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); + const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; data && newarray.push(...DocListCast(data)); const sidebar = d[fieldKey + '_sidebar']; diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 8daa69890..fa1a911f7 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,5 +1,5 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr'; -import { Field } from '../../fields/Doc'; +// import { Field } from '../../fields/Doc'; let serializing = 0; export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) { @@ -7,12 +7,16 @@ export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, cb(err, newValue); serializing--; } + +const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; +const reverseMap: { [ctor: string]: string } = {}; + export namespace SerializationHelper { export function IsSerializing() { return serializing > 0; } - export function Serialize(obj: Field): any { + export function Serialize(obj: any /* Field */): any { if (obj === undefined || obj === null) { return null; } @@ -24,7 +28,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); @@ -52,25 +56,24 @@ export namespace SerializationHelper { } const type = serializationTypes[obj.__type]; - const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result))); + const value = await new Promise(res => { + deserialize(type.ctor, obj, (err, result) => res(result)); + }); type.afterDeserialize?.(value); return value; } } -const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; -const reverseMap: { [ctor: string]: string } = {}; - 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 || constructorArgs) { - setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) }); + function addToMap(className: string, Ctor: { new (...args: any[]): any }) { + const schema = getDefaultModelSchema(Ctor) as any; + if (schema.targetClass !== Ctor || constructorArgs) { + setDefaultModelSchema(Ctor, { ...schema, factory: (context: any) => new Ctor(...(constructorArgs ?? []).map(arg => context.json[arg])) }); } if (!(className in serializationTypes)) { - serializationTypes[className] = { ctor, afterDeserialize }; - reverseMap[ctor.name] = className; + serializationTypes[className] = { ctor: Ctor, afterDeserialize }; + reverseMap[Ctor.name] = className; } else { throw new Error(`Name ${className} has already been registered as deserializable`); } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 8594a1c92..e2971895a 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,11 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { BsGoogle } from 'react-icons/bs'; import { FaFillDrip, FaPalette } from 'react-icons/fa'; -import { Utils, addStyleSheet, addStyleSheetRule } from '../../Utils'; +import { ClientUtils, addStyleSheet, addStyleSheetRule } from '../../ClientUtils'; import { Doc, Opt } from '../../fields/Doc'; import { DashVersion } from '../../fields/DocSymbols'; import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types'; @@ -18,6 +18,7 @@ import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; import { undoBatch } from './UndoManager'; +import { SnappingManager } from './SnappingManager'; export enum ColorScheme { Dark = 'Dark', @@ -34,6 +35,7 @@ export enum freeformScrollMode { @observer export class SettingsManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); @observable public isOpen = false; @@ -60,6 +62,16 @@ export class SettingsManager extends React.Component<{}> { } // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) }); + reaction( + () => [SettingsManager.userBackgroundColor, SettingsManager.userColor, SettingsManager.userVariantColor], + ([back, user, variant]) => { + SnappingManager.userBackgroundColor = back; + SnappingManager.userVariantColor = variant; + SnappingManager.userColor = user; + }, + { fireImmediately: true } + ); + SnappingManager.SettingsStyle = SettingsManager._settingsStyle; } matchSystem = () => { if (Doc.UserDoc().userThemeSystem) { @@ -116,7 +128,7 @@ export class SettingsManager extends React.Component<{}> { if (this.playgroundMode) { DocServer.Control.makeReadOnly(); addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else Doc.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); + } else ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); }); @undoBatch @@ -176,7 +188,7 @@ export class SettingsManager extends React.Component<{}> { {userTheme === ColorScheme.Custom && ( <Group formLabel="Custom Theme"> <ColorPicker - tooltip={'User Color'} // + tooltip="User Color" // color={SettingsManager.userColor} type={Type.SEC} icon={<FaFillDrip />} @@ -185,7 +197,7 @@ export class SettingsManager extends React.Component<{}> { setFinalColor={this.switchUserColor} /> <ColorPicker - tooltip={'User Background Color'} + tooltip="User Background Color" color={SettingsManager.userColor} type={Type.SEC} icon={<FaPalette />} @@ -194,7 +206,7 @@ export class SettingsManager extends React.Component<{}> { setFinalColor={this.switchUserBackgroundColor} /> <ColorPicker - tooltip={'User Variant Color'} + tooltip="User Variant Color" color={SettingsManager.userColor} type={Type.SEC} icon={<FaPalette />} @@ -212,8 +224,8 @@ export class SettingsManager extends React.Component<{}> { return ( <div className="prefs-content"> <Toggle - formLabel={'Show document header'} - formLabelPlacement={'right'} + formLabel="Show document header" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} @@ -221,8 +233,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Show Full Toolbar'} - formLabelPlacement={'right'} + formLabel="Show Full Toolbar" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} @@ -230,8 +242,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Show Button Labels'} - formLabelPlacement={'right'} + formLabel="Show Button Labels" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)} toggleStatus={FontIconBox.ShowIconLabels} @@ -239,8 +251,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Recognize Ink Gestures'} - formLabelPlacement={'right'} + formLabel="Recognize Ink Gestures" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)} toggleStatus={GestureOverlay.RecognizeGestures} @@ -248,8 +260,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Hide Labels In Ink Shapes'} - formLabelPlacement={'right'} + formLabel="Hide Labels In Ink Shapes" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} @@ -257,8 +269,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Open Ink Docs in Lightbox'} - formLabelPlacement={'right'} + formLabel="Open Ink Docs in Lightbox" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} @@ -266,8 +278,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> <Toggle - formLabel={'Show Link Lines'} - formLabelPlacement={'right'} + formLabel="Show Link Lines" + formLabelPlacement="right" toggleType={ToggleType.SWITCH} onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)} toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)} @@ -278,12 +290,12 @@ export class SettingsManager extends React.Component<{}> { <NumberDropdown number={NumCast(Doc.UserDoc().headerHeight, 30)} color={SettingsManager.userColor} - numberDropdownType={'slider'} + numberDropdownType="slider" min={6} max={60} step={2} type={Type.TERT} - unit={'px'} + unit="px" setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))} /> </Group> @@ -316,7 +328,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-title">Text</div> <div className="tab-column-content"> {/* <NumberInput/> */} - <Group formLabel={'Default Font'}> + <Group formLabel="Default Font"> <NumberDropdown color={SettingsManager.userColor} numberDropdownType="slider" @@ -325,7 +337,7 @@ export class SettingsManager extends React.Component<{}> { step={2} type={Type.PRIM} number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))} - unit={'px'} + unit="px" setNumber={val => (Doc.UserDoc().fontSize = val + 'px')} /> <Dropdown @@ -373,12 +385,12 @@ export class SettingsManager extends React.Component<{}> { @computed get passwordContent() { return ( <div className="password-content"> - <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> - <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> - <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> + <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> + <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> + <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} - <Button type={Type.SEC} text={'Forgot Password'} color={SettingsManager.userColor} /> - <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={SettingsManager.userColor} /> + <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} /> + <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} /> </div> ); } @@ -386,7 +398,7 @@ export class SettingsManager extends React.Component<{}> { @computed get accountOthersContent() { return ( <div className="account-others-content"> - <Button type={Type.TERT} text={'Connect to Google'} iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} /> + <Button type={Type.TERT} text="Connect to Google" iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} /> </div> ); } @@ -417,7 +429,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-title">Modes</div> <div className="tab-column-content"> <Dropdown - formLabel={'Mode'} + formLabel="Mode" closeOnSelect={true} items={[ { @@ -442,7 +454,7 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} fillWidth /> - <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> + <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> </div> <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}> Freeform Navigation @@ -475,10 +487,10 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column"> <div className="tab-column-title">Permissions</div> <div className="tab-column-content"> - <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} /> + <Button text="Manage Groups" type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} /> <Toggle toggleType={ToggleType.SWITCH} - formLabel={'Default access private'} + formLabel="Default access private" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} @@ -525,14 +537,14 @@ export class SettingsManager extends React.Component<{}> { <div className="settings-user"> <div style={{ color: SettingsManager.userBackgroundColor }}>{DashVersion}</div> <div className="settings-username" style={{ color: SettingsManager.userBackgroundColor }}> - {Doc.CurrentUserEmail} + {ClientUtils.CurrentUserEmail} </div> - <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> + <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(ClientUtils.prepend('/logout'))} /> </div> </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} /> </div> <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index fddf735e3..03f7e9b71 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -6,13 +6,14 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; +import { ClientUtils } from '../../ClientUtils'; +import { Utils } from '../../Utils'; import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; -import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; -import { Utils } from '../../Utils'; +import { GetEffectiveAcl, SharingPermissions, TraceMobx, distributeAcls, normalizeEmail } from '../../fields/util'; import { DocServer } from '../DocServer'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; @@ -84,7 +85,6 @@ export class SharingManager extends React.Component<{}> { @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all @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 - @observable private _buttonDown = false; // private get linkVisible() { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; @@ -136,9 +136,11 @@ export class SharingManager extends React.Component<{}> { populateUsers = async () => { if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { this.populating = true; - const userList = await RequestPromise.get(Utils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail); - runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users')); + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); raw.map( action((newUser: User) => { @@ -191,7 +193,8 @@ export class SharingManager extends React.Component<{}> { this.users .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) .forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + if (permission !== SharingPermissions.None) + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists }); } @@ -231,7 +234,7 @@ export class SharingManager extends React.Component<{}> { shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { if (layout) this.layoutDocAcls = true; if (shareWith !== 'Guest') { - const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith)); + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail : shareWith)); docs.forEach(doc => { if (user) this.setInternalSharing(user, permission, doc); else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); @@ -300,7 +303,7 @@ export class SharingManager extends React.Component<{}> { * Copies the Public sharing url to the user's clipboard. */ private copyURL = (e: any) => { - Utils.CopyText(Utils.shareUrl(this.targetDoc![Id])); + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); }; /** @@ -496,19 +499,19 @@ export class SharingManager extends React.Component<{}> { const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); // the owner of the doc and the current user are placed at the top of the user list. - const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`; + const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; const curUserPermission = StrCast(targetDoc[userKey]); // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? ( <div key={'owner'} className={'container'}> - <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span> + <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span> <div className="edit-actions"> <div className={'permissions-dropdown'}>Owner</div> </div> </div> ) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? ( <div key={'me'} className={'container'}> <span className={'padding'}>Me</span> <div className="edit-actions"> diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 359140732..eb47bbe88 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,7 +1,7 @@ -import { observable, action, runInAction, reaction, makeObservable } from 'mobx'; -import { Doc, Opt } from '../../fields/Doc'; +import { observable, action, runInAction, makeObservable } from 'mobx'; export class SnappingManager { + // eslint-disable-next-line no-use-before-define private static _manager: SnappingManager; private static get Instance() { return SnappingManager._manager ?? new SnappingManager(); @@ -12,7 +12,7 @@ export class SnappingManager { @observable _metaKey = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; - @observable _isResizing: Doc | undefined = undefined; + @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized @observable _canEmbed: boolean = false; @observable _horizSnapLines: number[] = []; @observable _vertSnapLines: number[] = []; @@ -23,7 +23,9 @@ export class SnappingManager { makeObservable(this); } - @action public static clearSnapLines = () => (this.Instance._vertSnapLines.length = this.Instance._horizSnapLines.length = 0); + @action public static clearSnapLines = () => { + this.Instance._vertSnapLines.length = this.Instance._horizSnapLines.length = 0; + }; @action public static addSnapLines = (horizLines: number[], vertLines: number[]) => { this.Instance._horizSnapLines.push(...horizLines); this.Instance._vertSnapLines.push(...vertLines); @@ -39,12 +41,17 @@ export class SnappingManager { public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore public static get CanEmbed() { return this.Instance._canEmbed; } // prettier-ignore public static get ExploreMode() { return this.Instance._exploreMode; } // prettier-ignore - public static SetShiftKey = (down: boolean) => runInAction(() => (this.Instance._shiftKey = down)); // prettier-ignore - public static SetCtrlKey = (down: boolean) => runInAction(() => (this.Instance._ctrlKey = down)); // prettier-ignore - public static SetMetaKey = (down: boolean) => runInAction(() => (this.Instance._metaKey = down)); // prettier-ignore - public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore - public static SetIsDragging = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore - public static SetIsResizing = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore - public static SetCanEmbed = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore - public static SetExploreMode = (state:boolean) => runInAction(() => (this.Instance._exploreMode = state)); // prettier-ignore + public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore + public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore + public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore + public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore + public static SetIsDragging = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore + public static SetIsResizing = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore + public static SetCanEmbed = (embed:boolean) => runInAction(() => {this.Instance._canEmbed = embed}); // prettier-ignore + public static SetExploreMode = (state:boolean) => runInAction(() => {this.Instance._exploreMode = state}); // prettier-ignore + + public static userColor: string | undefined; + public static userVariantColor: string | undefined; + public static userBackgroundColor: string | undefined; + public static SettingsStyle: any; } diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 857ca852f..4e941508d 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,7 +1,8 @@ -import { observable, action, runInAction } from 'mobx'; -import { Doc, Field } from '../../fields/Doc'; -import { RichTextField } from '../../fields/RichTextField'; +import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; +import { RichTextField } from '../../fields/RichTextField'; + +export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -97,7 +98,7 @@ export namespace UndoManager { export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { - Doc.MyDockedBtns.linearView_IsOpen && + printToConsole && console.log( ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + @@ -172,7 +173,7 @@ export namespace UndoManager { } export function StartBatch(batchName: string): Batch { - Doc.MyDockedBtns.linearView_IsOpen && console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName); runInAction(() => batchCounter.set(batchCounter.get() + 1)); if (currentBatch === undefined) { currentBatch = []; @@ -182,7 +183,7 @@ export namespace UndoManager { const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); - Doc.MyDockedBtns.linearView_IsOpen && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index 8fc6de6f9..6bbf55e5a 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -1,4 +1,7 @@ -import { Point } from "../../pen-gestures/ndollar"; +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-param-reassign */ +/* eslint-disable camelcase */ +import { Point } from '../../pen-gestures/ndollar'; class SmartRect { minx: number = 0; @@ -6,20 +9,43 @@ class SmartRect { maxx: number = 0; maxy: number = 0; - constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) { this.minx = mix; this.miny = miy; this.maxx = max; this.maxy = may; } + constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) { + this.minx = mix; + this.miny = miy; + this.maxx = max; + this.maxy = may; + } - public get Center() { return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0); } - public get TopLeft() { return new Point(this.minx, this.miny); } - public get TopRight() { return new Point(this.maxx, this.miny); } - public get BotLeft() { return new Point(this.minx, this.maxy); } - public get BotRight() { return new Point(this.maxx, this.maxy); } - public get Width() { return this.maxx - this.minx; } - public get Height() { return this.maxy - this.miny; } - public static Intersect(a: SmartRect, b: SmartRect) { return a.Intersect(b); } - public Intersect(b: SmartRect) { return !((this.minx > b.maxx) || (this.miny > b.maxy) || (b.minx > this.maxx) || (b.miny > this.maxy)); } + public get Center() { + return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0); + } + public get TopLeft() { + return new Point(this.minx, this.miny); + } + public get TopRight() { + return new Point(this.maxx, this.miny); + } + public get BotLeft() { + return new Point(this.minx, this.maxy); + } + public get BotRight() { + return new Point(this.maxx, this.maxy); + } + public get Width() { + return this.maxx - this.minx; + } + public get Height() { + return this.maxy - this.miny; + } + public static Intersect(a: SmartRect, b: SmartRect) { + return a.Intersect(b); + } + public Intersect(b: SmartRect) { + return !(this.minx > b.maxx || this.miny > b.maxy || b.minx > this.maxx || b.miny > this.maxy); + } public ContainsPercentage(other: SmartRect, axis: Point) { - var ret = 0; + let ret = 0; const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y); const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y); ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0; @@ -36,7 +62,7 @@ class SmartRect { if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx]; if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; - for (const pt of p) { + p.forEach(pt => { if (pt.X < r.minx) { r.minx = pt.X; } else if (pt.X > r.maxx) { @@ -47,7 +73,7 @@ class SmartRect { } else if (pt.Y > r.maxy) { r.maxy = pt.Y; } - } + }); } return r; } @@ -64,18 +90,18 @@ export function Normalize(p: Point) { function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) { const uPrime = new Array<number>(last - first + 1); // New parameter values - for (var i = first; i <= last; i++) { + for (let i = first; i <= last; i++) { uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]); } return uPrime; } function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) { - var maxError = 0; // Maximum error - var splitPoint2D = (last - first + 1) / 2; - for (var i = first + 1; i < last; i++) { - const P = [0, 0]; // point on curve + let maxError = 0; // Maximum error + let splitPoint2D = (last - first + 1) / 2; + for (let i = first + 1; i < last; i++) { + const P = [0, 0]; // point on curve EvalBezierFast(bezCurve, u[i - first], P); - const dx = P[0] - d[i].X;// offset from point to curve + const dx = P[0] - d[i].X; // offset from point to curve const dy = P[1] - d[i].Y; const dist = Math.sqrt(dx * dx + dy * dy); // Current error if (dist >= maxError) { @@ -88,11 +114,11 @@ function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Poin return { maxError, splitPoint2D }; } function ChordLengthParameterize(d: Point[], first: number, last: number) { - const u = new Array<number>(last - first + 1);// Parameterization + const u = new Array<number>(last - first + 1); // Parameterization - var prev = 0.0; + let prev = 0.0; u[0] = prev; - for (var i = first + 1; i <= last; i++) { + for (let i = first + 1; i <= last; i++) { const lastd = d[i - 1]; const curd = d[i]; const dx = lastd.X - curd.X; @@ -101,27 +127,38 @@ function ChordLengthParameterize(d: Point[], first: number, last: number) { } const ulastfirst = u[last - first]; - for (var i = first + 1; i <= last; i++) { + for (let i = first + 1; i <= last; i++) { u[i - first] /= ulastfirst; } return u; } /* -* B0, B1, B2, B3 : -* Bezier multipliers -*/ -function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; } -function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; } -function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; } -function B3(u: number) { return u * u * u; } + * B0, B1, B2, B3 : + * Bezier multipliers + */ +function B0(u: number) { + const tmp = 1.0 - u; + return tmp * tmp * tmp; +} +function B1(u: number) { + const tmp = 1.0 - u; + return 3 * u * tmp * tmp; +} +function B2(u: number) { + const tmp = 1.0 - u; + return 3 * u * u * tmp; +} +function B3(u: number) { + return u * u * u; +} function bounds(p: Point[]) { const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal - if (r.minx > r.maxx) (r.minx, r.maxx); + if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx]; if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max - for (var i = 1; i < 3; i++) { + for (let i = 1; i < 3; i++) { if (p[i].X < r.minx) r.minx = p[i].X; else if (p[i].X > r.maxx) r.maxx = p[i].X; @@ -131,37 +168,36 @@ function bounds(p: Point[]) { return r; } - function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { const sz = 4; const Vtemp = new Array<Array<Point>>(4); - for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4); + for (let i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4); /* Copy control points */ // std::copy(p.begin(), p.end(), Vtemp[0]); - for (var i = 0; i < sz; i++) { + for (let i = 0; i < sz; i++) { Vtemp[0][i].X = p[i].X; Vtemp[0][i].Y = p[i].Y; } /* Triangle computation */ - for (var i = 1; i < sz; i++) { - for (var j = 0; j < sz - i; j++) { + for (let i = 1; i < sz; i++) { + for (let j = 0; j < sz - i; j++) { const a = Vtemp[i - 1][j]; const b = Vtemp[i - 1][j + 1]; Vtemp[i][j].X = b.X * t + a.X * (1 - t); - Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t); + Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t); } } if (left) { - for (var j = 0; j < sz; j++) { + for (let j = 0; j < sz; j++) { left[j].X = Vtemp[j][0].X; left[j].Y = Vtemp[j][0].Y; } } if (right) { - for (var j = 0; j < sz; j++) { + for (let j = 0; j < sz; j++) { right[j].X = Vtemp[sz - 1 - j][j].X; right[j].Y = Vtemp[sz - 1 - j][j].Y; } @@ -169,51 +205,53 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) { } /* -* Recursively intersect two curves keeping track of their real parameters -* and depths of intersection. -* The results are returned in a 2-D array of doubles indicating the parameters -* for which intersections are found. The parameters are in the order the -* intersections were found, which is probably not in sorted order. -* When an intersection is found, the parameter value for each of the two -* is stored in the index elements array, and the index is incremented. -* -* If either of the curves has subdivisions left before it is straight -* (depth > 0) -* that curve (possibly both) is (are) subdivided at its (their) midpoint(s). -* the depth(s) is (are) decremented, and the parameter value(s) corresponding -* to the midpoints(s) is (are) computed. -* Then each of the subcurves of one curve is intersected with each of the -* subcurves of the other curve, first by testing the bounding boxes for -* interference. If there is any bounding box interference, the corresponding -* subcurves are recursively intersected. -* -* If neither curve has subdivisions left, the line segments from the first -* to last control point of each segment are intersected. (Actually the -* only the parameter value corresponding to the intersection point is found). -* -* The apriori flatness test is probably more efficient than testing at each -* level of recursion, although a test after three or four levels would -* probably be worthwhile, since many curves become flat faster than their -* asymptotic rate for the first few levels of recursion. -* -* The bounding box test fails much more frequently than it succeeds, providing -* substantial pruning of the search space. -* -* Each (sub)curve is subdivided only once, hence it is not possible that for -* one final line intersection test the subdivision was at one level, while -* for another final line intersection test the subdivision (of the same curve) -* was at another. Since the line segments share endpoints, the intersection -* is robust: a near-tangential intersection will yield zero or two -* intersections. -*/ + * Recursively intersect two curves keeping track of their real parameters + * and depths of intersection. + * The results are returned in a 2-D array of doubles indicating the parameters + * for which intersections are found. The parameters are in the order the + * intersections were found, which is probably not in sorted order. + * When an intersection is found, the parameter value for each of the two + * is stored in the index elements array, and the index is incremented. + * + * If either of the curves has subdivisions left before it is straight + * (depth > 0) + * that curve (possibly both) is (are) subdivided at its (their) midpoint(s). + * the depth(s) is (are) decremented, and the parameter value(s) corresponding + * to the midpoints(s) is (are) computed. + * Then each of the subcurves of one curve is intersected with each of the + * subcurves of the other curve, first by testing the bounding boxes for + * interference. If there is any bounding box interference, the corresponding + * subcurves are recursively intersected. + * + * If neither curve has subdivisions left, the line segments from the first + * to last control point of each segment are intersected. (Actually the + * only the parameter value corresponding to the intersection point is found). + * + * The apriori flatness test is probably more efficient than testing at each + * level of recursion, although a test after three or four levels would + * probably be worthwhile, since many curves become flat faster than their + * asymptotic rate for the first few levels of recursion. + * + * The bounding box test fails much more frequently than it succeeds, providing + * substantial pruning of the search space. + * + * Each (sub)curve is subdivided only once, hence it is not possible that for + * one final line intersection test the subdivision was at one level, while + * for another final line intersection test the subdivision (of the same curve) + * was at another. Since the line segments share endpoints, the intersection + * is robust: a near-tangential intersection will yield zero or two + * intersections. + */ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) { if (deptha > 0) { - const a1 = new Array<Point>(4), a2 = new Array<Point>(4); + const a1 = new Array<Point>(4); + const a2 = new Array<Point>(4); splitCubic(a, 0.5, a1, a2); const tmid = (t0 + t1) * 0.5; deptha--; if (depthb > 0) { - const b1 = new Array<Point>(4), b2 = new Array<Point>(4); + const b1 = new Array<Point>(4); + const b2 = new Array<Point>(4); splitCubic(b, 0.5, b1, b2); const umid = (u0 + u1) * 0.5; depthb--; @@ -229,8 +267,7 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe if (SmartRect.Intersect(bounds(a2), bounds(b2))) { recursively_intersect(a2, tmid, t1, deptha, b2, umid, u1, depthb, parameters); } - } - else { + } else { if (SmartRect.Intersect(bounds(a1), bounds(b))) { recursively_intersect(a1, t0, tmid, deptha, b, u0, u1, depthb, parameters); } @@ -238,50 +275,45 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe recursively_intersect(a2, tmid, t1, deptha, b, u0, u1, depthb, parameters); } } - } + } else if (depthb > 0) { + const b1 = new Array<Point>(4); + const b2 = new Array<Point>(4); + splitCubic(b, 0.5, b1, b2); + const umid = (u0 + u1) * 0.5; + depthb--; + if (SmartRect.Intersect(bounds(a), bounds(b1))) { + recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters); + } + if (SmartRect.Intersect(bounds(a), bounds(b2))) { + recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters); + } + } // Both segments are fully subdivided; now do line segments else { - if (depthb > 0) { - const b1 = new Array<Point>(4), b2 = new Array<Point>(4); - splitCubic(b, 0.5, b1, b2); - const umid = (u0 + u1) * 0.5; - depthb--; - if (SmartRect.Intersect(bounds(a), bounds(b1))) { - recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters); - } - if (SmartRect.Intersect(bounds(a), bounds(b2))) { - recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters); - } + const xlk = a[3].X - a[0].X; + const ylk = a[3].Y - a[0].Y; + const xnm = b[3].X - b[0].X; + const ynm = b[3].Y - b[0].Y; + const xmk = b[0].X - a[0].X; + const ymk = b[0].Y - a[0].Y; + const det = xnm * ylk - ynm * xlk; + if (1.0 + det === 1.0) { + return; } - else // Both segments are fully subdivided; now do line segments - { - const xlk = a[3].X - a[0].X; - const ylk = a[3].Y - a[0].Y; - const xnm = b[3].X - b[0].X; - const ynm = b[3].Y - b[0].Y; - const xmk = b[0].X - a[0].X; - const ymk = b[0].Y - a[0].Y; - const det = xnm * ylk - ynm * xlk; - if (1.0 + det === 1.0) { - return; - } - else { - const detinv = 1.0 / det; - const s = (xnm * ymk - ynm * xmk) * detinv; - const t = (xlk * ymk - ylk * xmk) * detinv; - if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) { - return; - } - parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]); - } + const detinv = 1.0 / det; + const s = (xnm * ymk - ynm * xmk) * detinv; + const t = (xlk * ymk - ylk * xmk) * detinv; + if (s < 0.0 || s > 1.0 || t < 0.0 || t > 1.0 || isNaN(s) || isNaN(t)) { + return; } + parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]); } } /* -* EvalBezier : -* Evaluate a Bezier curve at a particular parameter value -* -*/ + * EvalBezier : + * Evaluate a Bezier curve at a particular parameter value + * + */ const MAX_DEGREE = 5; function EvalBezier(V: Point[], degree: number, t: number, result: number[]) { if (degree + 1 > MAX_DEGREE) { @@ -293,35 +325,36 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) { const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points /* Copy array */ - for (var i = 0; i <= degree; i++) { + for (let i = 0; i <= degree; i++) { Vtemp[i].X = V[i].X; Vtemp[i].Y = V[i].Y; } /* Triangle computation */ - for (var i = 1; i <= degree; i++) { - for (var j = 0; j <= degree - i; j++) { + for (let i = 1; i <= degree; i++) { + for (let j = 0; j <= degree - i; j++) { Vtemp[j].X = (1.0 - t) * Vtemp[j].X + t * Vtemp[j + 1].X; Vtemp[j].Y = (1.0 - t) * Vtemp[j].Y + t * Vtemp[j + 1].Y; } } result[0] = Vtemp[0].X; - result[1] = Vtemp[0].Y;// Point on curve at parameter t + result[1] = Vtemp[0].Y; // Point on curve at parameter t } function EvalBezierFast(p: Point[], t: number, result: number[]) { const n = 3; const u = 1.0 - t; - var bc = 1, tn = 1; - var tmpX = p[0].X * u; - var tmpY = p[0].Y * u; - tn = tn * t; - bc = bc * (n - 1 + 1) / 1; + let bc = 1; + let tn = 1; + let tmpX = p[0].X * u; + let tmpY = p[0].Y * u; + tn *= t; + bc = (bc * (n - 1 + 1)) / 1; tmpX = (tmpX + tn * bc * p[1].X) * u; tmpY = (tmpY + tn * bc * p[1].Y) * u; - tn = tn * t; - bc = bc * (n - 2 + 1) / 2; + tn *= t; + bc = (bc * (n - 2 + 1)) / 2; tmpX = (tmpX + tn * bc * p[2].X) * u; tmpY = (tmpY + tn * bc * p[2].Y) * u; @@ -329,9 +362,9 @@ function EvalBezierFast(p: Point[], t: number, result: number[]) { result[1] = tmpY + tn * t * p[3].Y; } /* -* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent : -*Approximate unit tangents at endpoints and "center" of digitized curve -*/ + * ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent : + *Approximate unit tangents at endpoints and "center" of digitized curve + */ function ComputeLeftTangent(d: Point[], end: number) { const use = 1; const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y); @@ -348,19 +381,19 @@ function ComputeCenterTangent(d: Point[], center: number) { } const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1]; const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1]; - var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0); + let tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0); if (tHatCenter === new Point(0, 0)) { - tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp(); + tHatCenter = new Point(-V1.Y, -V1.X); // V1.Perp(); } return Normalize(tHatCenter); } function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) { const nPts = last - first + 1; // Number of pts in sub-curve - const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2); - const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2); + const Ax = new Array<number>(nPts * 2); // Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2); + const Ay = new Array<number>(nPts * 2); // Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2); /* Compute the A's */ - for (var i = 0; i < nPts; i++) { + for (let i = 0; i < nPts; i++) { const uprime = uPrime[i]; const b1 = B1(uprime); const b2 = B2(uprime); @@ -371,39 +404,42 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[ } /* Create the C and X matrices */ - const C = [[0, 0], [0, 0]]; + const C = [ + [0, 0], + [0, 0], + ]; const df = d[first]; const dl = d[last]; - const X = [0, 0]; // Matrix X - for (var i = 0; i < nPts; i++) { - C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; //A[i+0*nPts].Dot(A[i+0*nPts]); - C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]); + const X = [0, 0]; // Matrix X + for (let i = 0; i < nPts; i++) { + C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; // A[i+0*nPts].Dot(A[i+0*nPts]); + C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts]; // A[i+0*nPts].Dot(A[i+1*nPts]); C[1][0] = C[0][1]; - C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]); + C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts]; // A[i+1*nPts].Dot(A[i+1*nPts]); const uprime = uPrime[i]; const b0plb1 = B0(uprime) + B1(uprime); const b2plb3 = B2(uprime) + B3(uprime); const df1 = d[first + i]; - const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3)); - const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3)); + const tmpX = df1.X - (df.X * b0plb1 + dl.X * b2plb3); + const tmpY = df1.Y - (df.Y * b0plb1 + dl.Y * b2plb3); X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp) - X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp) + X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; // A[i+1*nPts].Dot(tmp) } /* Compute the determinants of C and X */ - const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12; + const det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1] || C[0][0] * C[1][1] * 10e-12; const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; /* Finally, derive alpha values */ - var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1; - var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1; + let alpha_l = det_C0_C1 === 0 ? 0.0 : det_X_C1 / det_C0_C1; + let alpha_r = det_C0_C1 === 0 ? 0.0 : det_C0_X / det_C0_C1; /* If alpha negative, use the Wu/Barsky heuristic (see text) */ /* (if alpha is 0, you get coincident control points that lead to - * divide by zero in any subsequent NewtonRaphsonRootFind() call. */ + * divide by zero in any subsequent NewtonRaphsonRootFind() call. */ const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y)); const epsilon = 1.0e-6 * segLength; if (alpha_l < epsilon || alpha_r < epsilon) { @@ -415,10 +451,10 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[ /* positioned exactly at the first and last data points */ /* Control points 1 and 2 are positioned an alpha distance out */ /* on the tangent vectors, left and right, respectively */ - result[0] = df;// RETURN bezier curve ctl pts + result[0] = df; // RETURN bezier curve ctl pts result[3] = dl; - result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l)); - result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r)); + result[1] = new Point(df.X + tHat1.X * alpha_l, df.Y + tHat1.Y * alpha_l); + result[2] = new Point(dl.X + tHat2.X * alpha_r, dl.Y + tHat2.Y * alpha_r); } /* @@ -426,21 +462,24 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[ * Use Newton-Raphson iteration to find better root. */ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) { - const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q'' - const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q'' + const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)]; + const Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q'' + const Q_u = [0, 0]; + const Q1_u = [0, 0]; + const Q2_u = [0, 0]; // u evaluated at Q, Q', & Q'' /* Compute Q(u) */ - var uPrime: number; // Improved u + let uPrime: number; // Improved u EvalBezierFast(Q, u, Q_u); /* Generate control vertices for Q' */ - for (var i = 0; i <= 2; i++) { + for (let i = 0; i <= 2; i++) { Q1[i].X = (Q[i + 1].X - Q[i].X) * 3.0; Q1[i].Y = (Q[i + 1].Y - Q[i].Y) * 3.0; } /* Generate control vertices for Q'' */ - for (var i = 0; i <= 1; i++) { + for (let i = 0; i <= 1; i++) { Q2[i].X = (Q1[i + 1].X - Q1[i].X) * 2.0; Q2[i].Y = (Q1[i + 1].Y - Q1[i].Y) * 2.0; } @@ -450,20 +489,20 @@ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) { EvalBezier(Q2, 1, u, Q2_u); /* Compute f(u)/f'(u) */ - const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]); - const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]); + const numerator = (Q_u[0] - P.X) * Q1_u[0] + (Q_u[1] - P.Y) * Q1_u[1]; + const denominator = Q1_u[0] * Q1_u[0] + Q1_u[1] * Q1_u[1] + (Q_u[0] - P.X) * Q2_u[0] + (Q_u[1] - P.Y) * Q2_u[1]; if (denominator === 0.0) { uPrime = u; - } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */ + } else uPrime = u - numerator / denominator; /* u = u - f(u)/f'(u) */ return uPrime; } function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) { - const bezCurve = new Array<Point>(4); // Control points of fitted Bezier curve - const maxIterations = 4; // Max times to try iterating + const bezCurve = new Array<Point>(4); // Control points of fitted Bezier curve + const maxIterations = 4; // Max times to try iterating const iterationError = error * error; // Error below which you try iterating - const nPts = last - first + 1; // Number of points in subset + const nPts = last - first + 1; // Number of points in subset /* Use heuristic if region only has two points in it */ if (nPts === 2) { @@ -471,8 +510,8 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: bezCurve[0] = d[first]; bezCurve[3] = d[last]; - bezCurve[1] = new Point(bezCurve[0].X + (tHat1.X * dist), bezCurve[0].Y + (tHat1.Y * dist)); - bezCurve[2] = new Point(bezCurve[3].X + (tHat2.X * dist), bezCurve[3].Y + (tHat2.Y * dist)); + bezCurve[1] = new Point(bezCurve[0].X + tHat1.X * dist, bezCurve[0].Y + tHat1.Y * dist); + bezCurve[2] = new Point(bezCurve[3].X + tHat2.X * dist, bezCurve[3].Y + tHat2.Y * dist); result.push(bezCurve[1]); result.push(bezCurve[2]); @@ -481,11 +520,11 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: } /* Parameterize points, and attempt to fit curve */ - var u = ChordLengthParameterize(d, first, last); + let u = ChordLengthParameterize(d, first, last); GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve); /* Find max deviation of points to fitted curve */ - const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error + const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error if (maxError < Math.abs(error)) { result.push(bezCurve[1]); result.push(bezCurve[2]); @@ -496,8 +535,8 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: /* If error not too large, try some reparameterization */ /* and iteration */ if (maxError < iterationError) { - for (var i = 0; i < maxIterations; i++) { - const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values + for (let i = 0; i < maxIterations; i++) { + const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve); const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime); if (maxError < error) { @@ -527,13 +566,13 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) { tHat1 = tHat1 ?? Normalize(ComputeLeftTangent(d, 0)); tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1)); tHat2 = new Point(-tHat2.X, -tHat2.Y); - var u = ChordLengthParameterize(d, 0, d.length - 1); + let u = ChordLengthParameterize(d, 0, d.length - 1); const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; - GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /* Find max deviation of points to fitted curve */ - var finalCtrls = bezCurveCtrls.slice(); - var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u); - for (var i = 0; i < 10; i++) { - const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values + GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /* Find max deviation of points to fitted curve */ + let finalCtrls = bezCurveCtrls.slice(); + let { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u); + for (let i = 0; i < 10; i++) { + const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls); const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime); if (maxError < error) { @@ -1428,4 +1467,4 @@ spliceR[0] += v; } #endif -*/
\ No newline at end of file +*/ diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 0c49aeed4..d6fd339a9 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,23 +1,24 @@ -import * as React from 'react'; -import * as uuid from 'uuid'; -import '.././SettingsManager.scss'; -import './ReportManager.scss'; -import ReactLoading from 'react-loading'; +import { Octokit } from '@octokit/core'; +import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; -import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { BsArrowsAngleContract, BsArrowsAngleExpand, BsX } from 'react-icons/bs'; import { CgClose } from 'react-icons/cg'; import { HiOutlineArrowLeft } from 'react-icons/hi'; -import { Issue } from './reportManagerSchema'; -import { observer } from 'mobx-react'; +import { MdRefresh } from 'react-icons/md'; +import ReactLoading from 'react-loading'; +import * as uuid from 'uuid'; +import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; -import { MainViewModal } from '../../views/MainViewModal'; -import { Octokit } from '@octokit/core'; -import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; -import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils'; -import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents'; import { StrCast } from '../../../fields/Types'; -import { MdRefresh } from 'react-icons/md'; +import { MainViewModal } from '../../views/MainViewModal'; +import '.././SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; +import './ReportManager.scss'; +import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; +import { Issue } from './reportManagerSchema'; +import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils'; /** * Class for reporting and viewing Github issues within the app. @@ -133,7 +134,7 @@ export class ReportManager extends React.Component<{}> { const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { owner: 'brown-dash', repo: 'Dash-Web', - title: formatTitle(this.formData.title, Doc.CurrentUserEmail), + title: formatTitle(this.formData.title, ClientUtils.CurrentUserEmail), body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`, labels: ['from-dash-app', this.formData.type, this.formData.priority], }); |
