From 2a313f28fcb8675223708b0657de7517a3281095 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 17 Apr 2024 12:27:21 -0400 Subject: restoring eslint - updates not complete yet --- src/client/util/CaptureManager.tsx | 2 +- src/client/util/CurrentUserUtils.ts | 250 ++++++--------- src/client/util/DictationManager.ts | 6 +- src/client/util/DocumentManager.ts | 80 ++--- src/client/util/DragManager.ts | 175 ++++++----- src/client/util/DropActionTypes.ts | 9 + src/client/util/GroupManager.tsx | 20 +- src/client/util/History.ts | 4 +- src/client/util/HypothesisUtils.ts | 4 +- src/client/util/Import & Export/ImageUtils.ts | 10 +- src/client/util/InteractionUtils.tsx | 317 ++++++------------- src/client/util/LinkFollower.ts | 4 +- src/client/util/LinkManager.ts | 49 ++- src/client/util/Scripting.ts | 52 ++-- src/client/util/ScriptingGlobals.ts | 47 +-- src/client/util/SearchUtil.ts | 6 +- src/client/util/SerializationHelper.ts | 29 +- src/client/util/SettingsManager.tsx | 86 +++--- src/client/util/SharingManager.tsx | 27 +- src/client/util/SnappingManager.ts | 31 +- src/client/util/UndoManager.ts | 13 +- src/client/util/bezierFit.ts | 395 +++++++++++++----------- src/client/util/reportManager/ReportManager.tsx | 31 +- 23 files changed, 799 insertions(+), 848 deletions(-) create mode 100644 src/client/util/DropActionTypes.ts (limited to 'src/client/util') 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(['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([ "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(['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(['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([]); 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(); @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 = 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 = 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, 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(); - 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(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(waitres => setTimeout(() => waitres())); + await new Promise(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; childDocView: Opt; 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(res => docView.iconify(res)); + // eslint-disable-next-line no-await-in-loop + await new Promise(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, docView: DocumentView, options: FocusViewOptions, contextView: Opt, targetDoc: Doc) { + restoreDocView(viewSpec: Opt, docViewIn: DocumentView, options: FocusViewOptions, contextView: Opt, 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(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('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('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; - 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('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('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 => { 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 { - 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) => void): MultiTouchEventDisposer { - const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent>).detail); - // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent>).detail) : undefined; - // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent>).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) => void): MultiTouchEventDisposer { - const handler = (e: Event) => func(e, (e as CustomEvent>).detail); - element.addEventListener('dashOnTouchHoldStart', handler); - return () => { - element.removeEventListener('dashOnTouchHoldStart', handler); - }; - } - - export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent, prevPoints: Map, ignorePen: boolean): React.Touch[] { - const myTouches = new Array(); - 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 { {!mask ? null : ( - + )} {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : ( @@ -172,7 +177,7 @@ export namespace InteractionUtils { 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, 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, 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) { +export function IsFollowLinkScript(field: FieldResult) { 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(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; 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; - 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 } } = {}; +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 } } = {}; -const reverseMap: { [ctor: string]: string } = {}; - export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise, 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 && ( } @@ -185,7 +197,7 @@ export class SettingsManager extends React.Component<{}> { setFinalColor={this.switchUserColor} /> } @@ -194,7 +206,7 @@ export class SettingsManager extends React.Component<{}> { setFinalColor={this.switchUserBackgroundColor} /> } @@ -212,8 +224,8 @@ export class SettingsManager extends React.Component<{}> { return (
(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} /> (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} /> (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)} toggleStatus={FontIconBox.ShowIconLabels} @@ -239,8 +251,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)} toggleStatus={GestureOverlay.RecognizeGestures} @@ -248,8 +260,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} @@ -257,8 +269,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} @@ -266,8 +278,8 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} /> (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)} toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)} @@ -278,12 +290,12 @@ export class SettingsManager extends React.Component<{}> { console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))} /> @@ -316,7 +328,7 @@ export class SettingsManager extends React.Component<{}> {
Text
{/* */} - + { 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')} /> { @computed get passwordContent() { return (
- this.changeVal(val as string, 'curr')} fillWidth password /> - this.changeVal(val as string, 'new')} fillWidth password /> - this.changeVal(val as string, 'conf')} fillWidth password /> + this.changeVal(val as string, 'curr')} fillWidth password /> + this.changeVal(val as string, 'new')} fillWidth password /> + this.changeVal(val as string, 'conf')} fillWidth password /> {!this.passwordResultText ? null :
{this.passwordResultText}
} -
); } @@ -386,7 +398,7 @@ export class SettingsManager extends React.Component<{}> { @computed get accountOthersContent() { return (
-
); } @@ -417,7 +429,7 @@ export class SettingsManager extends React.Component<{}> {
Modes
{ color={SettingsManager.userColor} fillWidth /> - +
Freeform Navigation @@ -475,10 +487,10 @@ export class SettingsManager extends React.Component<{}> {
Permissions
-
-
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 ? (
- {targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)} + {targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}
Owner
) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? (
Me
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) => 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(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(last - first + 1);// Parameterization + const u = new Array(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>(4); - for (var i = 0; i < 4; i++) Vtemp[i] = new Array(4); + for (let i = 0; i < 4; i++) Vtemp[i] = new Array(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(4), a2 = new Array(4); + const a1 = new Array(4); + const a2 = new Array(4); splitCubic(a, 0.5, a1, a2); const tmid = (t0 + t1) * 0.5; deptha--; if (depthb > 0) { - const b1 = new Array(4), b2 = new Array(4); + const b1 = new Array(4); + const b2 = new Array(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(4); + const b2 = new Array(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(4), b2 = new Array(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(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); - const Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + const Ax = new Array(nPts * 2); // Precomputed rhs for eqn //std::vector A(nPts * 2); + const Ay = new Array(nPts * 2); // Precomputed rhs for eqn //std::vector 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(4); // Control points of fitted Bezier curve - const maxIterations = 4; // Max times to try iterating + const bezCurve = new Array(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], }); -- cgit v1.2.3-70-g09d2 From b6229b0a6141afbfd0e78e3ec870218187864def Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 19 Apr 2024 11:02:05 -0400 Subject: fixed text search highlighting. fixed first typed characfter of note to have marks. --- package-lock.json | 2 +- src/client/apis/gpt/GPT.ts | 8 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/util/DropConverter.ts | 77 ++--- src/client/views/nodes/ComparisonBox.tsx | 28 +- src/client/views/nodes/KeyValueBox.tsx | 61 ++-- .../nodes/formattedText/FormattedTextBox.scss | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 346 +++++++++++---------- .../formattedText/FormattedTextBoxComment.tsx | 2 +- src/client/views/nodes/formattedText/marks_rts.ts | 6 +- src/client/views/pdf/AnchorMenu.tsx | 6 +- src/fields/Doc.ts | 160 +++++++--- src/fields/util.ts | 2 +- webpack.config.js | 2 +- 14 files changed, 399 insertions(+), 305 deletions(-) (limited to 'src/client/util') diff --git a/package-lock.json b/package-lock.json index d366bb717..cc1a0ea64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -296,7 +296,7 @@ "webpack-dev-server": "^5.0.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 63563cb79..e3e87e017 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -27,9 +27,9 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { */ let lastCall = ''; let lastResp = ''; -const gptAPICall = async (inputText: string, callType: GPTCallType) => { - if (callType === GPTCallType.SUMMARY) inputText += '.'; - const opts: GPTCallOpts = callTypeMap[callType]; +const gptAPICall = async (inputTextIn: string, callType: GPTCallType) => { + const inputText = callType === GPTCallType.SUMMARY ? inputTextIn + '.' : inputTextIn; + const opts = callTypeMap[callType]; if (lastCall === inputText) return lastResp; try { const configuration: ClientOptions = { @@ -69,8 +69,8 @@ const gptImageCall = async (prompt: string, n?: number) => { // return response.data.data[0].url; } catch (err) { console.error(err); - return; } + return undefined; }; export { gptAPICall, gptImageCall, GPTCallType }; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 7811f8605..535f7cf9d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1057,4 +1057,4 @@ ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.import // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() }); \ No newline at end of file +ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() }); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index ed5749d06..0314af06b 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,10 +1,9 @@ -import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; -import { listSpec } from '../../fields/Schema'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; -import { Cast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; @@ -12,21 +11,6 @@ import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox' import { DragManager } from './DragManager'; import { ScriptingGlobals } from './ScriptingGlobals'; -/** - * Converts a Doc to a render template that can be applied to other Docs to customize how they render while - * still using the other Doc as the backing data store (ie, dataDoc). During rendering, if a layout Doc is provided - * with 'isTemplateDoc' set, then the layout Doc is treated as a template for the rendered Doc. The template Doc is - * "expanded" to create an template instance for the rendered Doc. - * - * - * @param doc the doc to convert to a template - * @returns 'doc' - */ -export function MakeTemplate(doc: Doc) { - doc.isTemplateDoc = makeTemplate(doc, true); - return doc; -} - /** * * Recursively converts 'doc' into a template that can be used to render other documents. @@ -63,26 +47,22 @@ function makeTemplate(doc: Doc, first: boolean = true): boolean { } return isTemplate; } -export function convertDropDataToButtons(data: DragManager.DocumentDragData) { - data?.draggedDocuments.map((doc, i) => { - let dbox = doc; - // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes(FontIconBox.name)) { - if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { - //dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon - dbox = Doc.MakeEmbedding(dbox); - const dragProps = Cast(dbox.dropPropertiesToRemove, listSpec('string'), []); - const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps)); - remProps.map(prop => (dbox[prop] = undefined)); - } - } else if (!doc.onDragStart && !doc.isButtonBar) { - dbox = makeUserTemplateButton(doc); - } else if (doc.isButtonBar) { - dbox.ignoreClick = true; - } - data.droppedDocuments[i] = dbox; - }); + +/** + * Converts a Doc to a render template that can be applied to other Docs to customize how they render while + * still using the other Doc as the backing data store (ie, dataDoc). During rendering, if a layout Doc is provided + * with 'isTemplateDoc' set, then the layout Doc is treated as a template for the rendered Doc. The template Doc is + * "expanded" to create an template instance for the rendered Doc. + * + * + * @param doc the doc to convert to a template + * @returns 'doc' + */ +export function MakeTemplate(doc: Doc) { + doc.isTemplateDoc = makeTemplate(doc, true); + return doc; } + export function makeUserTemplateButton(doc: Doc) { const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.type !== DocumentType.FONTICON) { @@ -106,7 +86,30 @@ export function makeUserTemplateButton(doc: Doc) { dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); return dbox; } +export function convertDropDataToButtons(data: DragManager.DocumentDragData) { + data?.draggedDocuments.forEach((doc, i) => { + let dbox = doc; + // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant + if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes(FontIconBox.name)) { + if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { + // dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon + dbox = Doc.MakeEmbedding(dbox); + const dragProps = StrListCast(dbox.dropPropertiesToRemove); + const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps)); + remProps.forEach(prop => { + dbox[prop] = undefined; + }); + } + } else if (!doc.onDragStart && !doc.isButtonBar) { + dbox = makeUserTemplateButton(doc); + } else if (doc.isButtonBar) { + dbox.ignoreClick = true; + } + data.droppedDocuments[i] = dbox; + }); +} ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }, diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 99f9c03bf..708536de0 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -52,13 +52,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @undoBatch private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { - const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; - const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.Document, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); - Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc); + const { droppedDocuments } = dropEvent.complete.docDragData; + const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); + Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; } + return undefined; }; private registerSliding = (e: React.PointerEvent, targetWidth: number) => { @@ -83,7 +84,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() // on click, animate slider movement to the targetWidth this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); setTimeout( - action(() => (this._animating = '')), + action(() => { + this._animating = ''; + }), 200 ); }) @@ -109,7 +112,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; - /* addAsAnnotation &&*/ this.addDocument(anchor); + /* addAsAnnotation && */ this.addDocument(anchor); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document); return anchor; } @@ -148,7 +151,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return true; }, emptyFunction, - e => this.clearDoc(which) + () => this.clearDoc(which) ); }; docStyleProvider = (doc: Opt, props: Opt, property: string): any => { @@ -213,15 +216,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() const displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot); - return targetDoc || layoutTemplateString ? ( + const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + return targetDoc || layoutString ? ( <> () hideLinkButton pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> - {layoutTemplateString ? null : clearButton(whichSlot)} + {layoutString ? null : clearButton(whichSlot)} // placeholder image if doc is missing ) : (
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 74773b244..b8296ce51 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -79,7 +80,8 @@ export class KeyValueBox extends ObservableReactComponent { * @param value * @returns */ - public static CompileKVPScript(rawvalue: string): KVPScript | undefined { + public static CompileKVPScript(rawvalueIn: string): KVPScript | undefined { + let rawvalue = rawvalueIn; const onDelegate = rawvalue.startsWith('='); rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false; @@ -87,7 +89,7 @@ export class KeyValueBox extends ObservableReactComponent { rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)'); const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`'; - var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); + let script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); if (!script.compiled) { script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer()); } @@ -153,20 +155,20 @@ export class KeyValueBox extends ObservableReactComponent { const ids: { [key: string]: string } = {}; const protos = Doc.GetAllPrototypes(doc); - for (const proto of protos) { + protos.forEach(proto => { Object.keys(proto).forEach(key => { if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { ids[key] = key; } }); - } + }); const rows: JSX.Element[] = []; let i = 0; const self = this; const keys = Object.keys(ids).slice(); - //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { - for (const key of keys.sort((a: string, b: string) => { + // for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { + const sortedKeys = keys.sort((a: string, b: string) => { const a_ = a.split('_')[0]; const b_ = b.split('_')[0]; if (a_ < b_) return -1; @@ -174,7 +176,8 @@ export class KeyValueBox extends ObservableReactComponent { if (a === a_) return -1; if (b === b_) return 1; return a === b ? 0 : a < b ? -1 : 1; - })) { + }); + sortedKeys.forEach(key => { rows.push( { keyName={key} /> ); - } + }); return rows; } @computed get newKeyValue() { @@ -229,7 +232,7 @@ export class KeyValueBox extends ObservableReactComponent { this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); }; @action - onDividerUp = (e: PointerEvent): void => { + onDividerUp = (): void => { document.removeEventListener('pointermove', this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); }; @@ -244,11 +247,11 @@ export class KeyValueBox extends ObservableReactComponent { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true }); - for (const row of rows) { + rows.forEach(row => { const field = this.createFieldView(DocCast(this._props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); - } + }); return parent; } return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined; @@ -256,22 +259,23 @@ export class KeyValueBox extends ObservableReactComponent { createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row._props.keyName; - const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); - fieldTemplate.title = metaKey; - fieldTemplate.layout_fitWidth = true; - fieldTemplate._xMargin = 10; - fieldTemplate._yMargin = 10; - fieldTemplate._width = 100; - fieldTemplate._height = 40; - fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); - return fieldTemplate; + const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); + fieldTempDoc.title = metaKey; + fieldTempDoc.layout_fitWidth = true; + fieldTempDoc._xMargin = 10; + fieldTempDoc._yMargin = 10; + fieldTempDoc._width = 100; + fieldTempDoc._height = 40; + fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey); + return fieldTempDoc; }; inferType = (data: FieldResult, metaKey: string) => { const options = { _width: 300, _height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') { return FormattedTextBox.LayoutString(metaKey); - } else if (data instanceof List) { + } + if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } @@ -280,21 +284,18 @@ export class KeyValueBox extends ObservableReactComponent { return Docs.Create.StackingDocument([], options); } switch (first.data.constructor) { - case RichTextField: - return Docs.Create.TreeDocument([], options); - case ImageField: - return Docs.Create.MasonryDocument([], options); - default: - console.log(`Template for ${first.data.constructor} not supported!`); - return undefined; - } + case RichTextField: return Docs.Create.TreeDocument([], options); + case ImageField: return Docs.Create.MasonryDocument([], options); + default: console.log(`Template for ${first.data.constructor} not supported!`); + return undefined; + } // prettier-ignore } else if (data instanceof ImageField) { return ImageBox.LayoutString(metaKey); } return new Doc(); }; - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const open = cm.findByDescription('Change Perspective...'); const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : []; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 38dd2e847..5b2b558fc 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -350,7 +350,7 @@ footnote::before { span { font-family: inherit; background-color: inherit; - display: contents; // fixes problem where extra space is added around
    lists when inside a prosemirror span + display: inline; // needs to be inline for search highlighting to appear // contents; // fixes problem where extra space is added around
      lists when inside a prosemirror span } blockquote { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 31252e0ab..c7543560d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from ' import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -82,6 +82,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent EditorState.create(FormattedTextBox.Instance.config); + // eslint-disable-next-line no-use-before-define public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; static _globalHighlightsCache: string = ''; @@ -100,7 +101,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + let allFoundLinkAnchors: any[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any /* , pos: number, parent: any */) => { const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; @@ -249,7 +245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); + const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); @@ -264,11 +260,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { AnchorMenu.Instance.Status = 'marquee'; - AnchorMenu.Instance.OnClick = (e: PointerEvent) => { + AnchorMenu.Instance.OnClick = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created }; - AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { + AnchorMenu.Instance.OnAudio = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); @@ -279,7 +275,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (stopFunc = stop)); + DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + const reactionDisposer = reaction( () => target.mediaState, dictation => { @@ -324,7 +321,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { @@ -339,7 +338,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && (this._editorView as any).docView) { + if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this.tryUpdateDoc(false); @@ -347,13 +346,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && (this._editorView as any).docView) { + if (this._editorView) { const { state } = this._editorView; const { dataDoc } = this; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box - const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); @@ -362,7 +360,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any /* , pos: number, parent: any */) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { accumTags.push(node.attrs.fieldKey); } @@ -417,37 +415,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { let linkTime; let linkAnchor; - let link; - LinkManager.Links(this.dataDoc).forEach((l, i) => { - const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; + LinkManager.Links(this.dataDoc).forEach(l => { + const anchor = DocCast(l.link_anchor_1)?.annotationOn ? DocCast(l.link_anchor_1) : DocCast(l.link_anchor_2)?.annotationOn ? DocCast(l.link_anchor_2) : undefined; if (anchor && (anchor.annotationOn as Doc).mediaState === mediaState.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; - link = l; } }); if (this._editorView && linkTime) { - const state = this._editorView.state; - const now = Date.now(); - let mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(now / 1000) }); - if (!this._break && state.selection.to !== state.selection.from) { - for (let i = state.selection.from; i <= state.selection.to; i++) { - const pos = state.doc.resolve(i); - const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); - if (um) { - mark = um; - break; - } - } - } - - const path = (this._editorView.state.selection.$from as any).path; - if (linkAnchor && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { + const { state } = this._editorView; + const { path } = state.selection.$from as any; + if (linkAnchor && path[path.length - 3].type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; - const from = state.selection.from; - const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); - const replaced = this._editorView.state.tr.insert(from - 1, value); + const { from } = state.selection; + const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); + const replaced = state.tr.insert(from - 1, value); this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); } } @@ -464,14 +447,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + let { tr } = this._editorView.state; + const { from, to } = this._editorView.state.selection; + const { autoLinkAnchor } = this._editorView.state.schema.marks; + tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor); + Doc.MyPublishedDocs.filter(term => term.title).forEach(term => { + tr = this.hyperlinkTerm(tr, term, newAutoLinks); + }); + tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); this._editorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); @@ -507,11 +490,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent) => { + hyperlinkTerm = (trIn: any, target: Doc, newAutoLinks: Set) => { + let tr = trIn; const editorView = this._editorView; - if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { + if (editorView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, ''); - var alink: Doc | undefined; + let alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { if ( !sel.$anchor.pos || @@ -523,7 +507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number /* , parent: any */) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? @@ -554,12 +538,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); - const length = res[0].length; - let tr = this._editorView.state.tr; + const { _editorView } = this; + if (_editorView && terms.some(t => t)) { + const { state } = _editorView; + let { tr } = state; + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const res = terms.filter(t => t).map(term => this.findInNode(_editorView, state.doc, term)); + const { length } = res[0]; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; @@ -574,18 +560,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark))); - flattened[lastSel] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); + flattened.forEach((h: TextSelection, ind: number) => { + tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark); + }); + flattened[lastSel] && _editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); } }; unhighlightSearchTerms = () => { - if (window.screen.width < 600) null; - else if (this._editorView && (this._editorView as any).docView) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const end = this._editorView.state.doc.nodeSize - 2; - this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + if (this._editorView) { + const { state } = this._editorView; + if (state) { + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const end = state.doc.nodeSize - 2; + this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + } } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { @@ -634,7 +624,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent -1) { const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1)); ret.push(sel); @@ -713,14 +702,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (ret = ret.concat(this.findInNode(pm, child, find)))); + node.content.forEach(child => { + ret = ret.concat(this.findInNode(pm, child, find)); + }); } return ret; } updateHighlights = (highlights: string[]) => { if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; - setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''))); + setTimeout(() => { + FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''); + }); clearStyleSheetRules(FormattedTextBox._userStyleSheet); if (!highlights.includes('Audio Tags')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); @@ -757,12 +750,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } + // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } @action @@ -820,7 +814,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._props.pinToPres(anchor, {}); @undoBatch - makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); + makeTargetToggle = (anchor: Doc) => { + anchor.followLinkToggle = !anchor.followLinkToggle; + }; @undoBatch showTargetTrail = (anchor: Doc) => { @@ -836,11 +832,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const cm = ContextMenu.Instance; - const editor = this._editorView!; - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let target = e.target as any; // hrefs are stored on the database of the node that wraps the hyerlink while (target && !target.dataset?.targethrefs) target = target.parentElement; - if (target && !(e.nativeEvent as any).dash) { + const editor = this._editorView; + if (editor && target && !(e.nativeEvent as any).dash) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -850,8 +845,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const sel = editor.state.selection; - editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + const { selection } = editor.state; + editor.dispatch(editor.state.tr.removeMark(selection.from, selection.to, editor.state.schema.marks.linkAnchor)); }); e.persist(); anchorDoc && @@ -887,7 +882,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50), 50); + setTimeout(() => { + this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; + }, 50); }), icon: 'eye', }); @@ -926,19 +923,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), + event: () => { + this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar; + }, icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); appearanceItems.push({ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', - event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), + event: () => { + this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI; + }, icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && appearanceItems.push({ description: 'Broadcast Message', - event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text)), + event: () => + DocServer.GetRefField('rtfProto').then(proto => { + proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text); + }), icon: 'expand-arrows-alt', }); @@ -960,19 +964,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' }); + optionItems.push({ + description: `Toggle auto update from template`, + event: () => { + this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']; + }, + icon: 'star', + }); optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); this._props.renderDepth && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + event: () => { + this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR; + }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, - event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), + event: () => { + this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; + }, icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); @@ -980,7 +994,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); - this._downX = this._downY = Number.NaN; }; animateRes = (resIndex: number, newText: string) => { @@ -995,7 +1008,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { try { - let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); + const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { console.error('GPT call failed'); this.animateRes(0, 'Something went wrong.'); @@ -1020,8 +1033,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + tr.doc.nodesBetween(selection.from, selection.to, (node: any, pos: number /* , parent: any */) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allAnchors = [{ href, title, anchorId: anchor[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); @@ -1102,7 +1116,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._sidebarRef?.current?.makeDocUnfiltered(doc)); } - return new Promise>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise>(res => { + DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)); + }); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; const textAnchorId = textAnchor[Id]; + let start = 0; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { + // eslint-disable-next-line no-use-before-define const examinedNode = findAnchorNode(node, editor); if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); @@ -1163,7 +1181,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent textAnchorId === item.href.replace(/.*\/doc\//, '')) ? { node, start: 0 } : undefined; }; - let start = 0; this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below if (this._editorView && textAnchorId) { const editor = this._editorView; @@ -1179,13 +1196,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); - setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed); + setTimeout(() => { + this._focusSpeed = undefined; + }, this._focusSpeed); setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; - } else { - return this._props.focus(this.Document, options); } + return this._props.focus(this.Document, options); } + return undefined; }; // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. @@ -1205,7 +1224,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }), - (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) + autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( () => Array.from(FormattedTextBox._globalHighlights).slice(), @@ -1214,7 +1233,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._props.PanelWidth(), - width => this.tryUpdateScrollHeight() + () => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), @@ -1348,7 +1367,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => { const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor'); return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId)); }; @@ -1381,7 +1400,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; + const { tr } = this._editorView.state; + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { @@ -1526,8 +1555,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return; //e.stopPropagation(); - } else (e.nativeEvent as any).handledByInnerReactInstance = true; + return; // e.stopPropagation(); + } + (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. @@ -1553,9 +1583,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - document.removeEventListener('pointerup', this.onSelectEnd); - }; + onSelectEnd = (): void => document.removeEventListener('pointerup', this.onSelectEnd); onPointerUp = (e: React.PointerEvent): void => { const state = this.EditorView?.state; if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { @@ -1607,7 +1632,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - //applyDevTools.applyDevTools(this._editorView); + // applyDevTools.applyDevTools(this._editorView); this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; @@ -1618,10 +1643,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent 4 || Math.abs(e.clientY - this._downY) > 4) { - this._forceDownNode = undefined; - return; - } if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1645,13 +1666,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; onKeyDown = (e: React.KeyboardEvent) => { + const { _editorView } = this; + if (!_editorView) return; if ((e.altKey || e.ctrlKey) && e.key === 't') { - e.preventDefault(); - e.stopPropagation(); this._props.setTitleFocus?.(); + StopEvent(e); return; } - const state = this._editorView!.state; + const { state } = _editorView; if (!state.selection.empty && e.key === '%') { this._rules!.EnteringStyle = true; - e.preventDefault(); - e.stopPropagation(); + StopEvent(e); return; } if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - let stopPropagation = true; - for (var i = state.selection.from; i <= state.selection.to; i++) { + for (let i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); @@ -1769,22 +1788,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type === schema.marks.user_mark && m.attrs.modified === modified); + _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified }))); } break; } - if (stopPropagation) e.stopPropagation(); + e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1804,6 +1823,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -1815,7 +1835,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + const setScrollHeight = () => { + this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight; + }; if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); @@ -1833,7 +1855,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); - setSidebarHeight = (height: number) => (this.dataDoc[this.SidebarKey + '_height'] = height); + setSidebarHeight = (height: number) => { + this.dataDoc[this.SidebarKey + '_height'] = height; + }; sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props @@ -1851,10 +1875,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this._recordingDictation = !this._recordingDictation)) + action(() => { + this._recordingDictation = !this._recordingDictation; + }) ) }> - +
); } @@ -1883,6 +1909,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}>
@@ -1963,7 +1991,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.cycleAlternateText())} style={{ display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -2000,7 +2028,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; + return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /* 'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -60,7 +60,7 @@ export const marks: { [index: string]: MarkSpec } = { }, }, ], - toDOM(node: any) { + toDOM() { return ['span', { 'data-noAutoLink': 'true' }, 0]; }, }, diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 9c4080154..02ff661e5 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -5,7 +5,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorResult } from 'react-color'; import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { unimplementedFunction } from '../../../Utils'; +import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -50,8 +50,8 @@ export class AnchorMenu extends AntimodeMenu { public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; - public Highlight: (color: string) => Opt = (color: string) => undefined; - public GetAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = (savedAnnotations: Opt>, addAsAnnotation: boolean) => undefined; + public Highlight: (color: string) => Opt = (/* color: string */) => undefined; + public GetAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = emptyFunction; public Delete: () => void = unimplementedFunction; public PinToPres: () => void = unimplementedFunction; public MakeTargetToggle: () => void = unimplementedFunction; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 60c6402d4..3fb914423 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,3 +1,5 @@ +/* eslint-disable default-param-last */ +/* eslint-disable no-use-before-define */ import { action, computed, makeObservable, observable, ObservableMap, ObservableSet, runInAction } from 'mobx'; import { computedFn } from 'mobx-utils'; import { alias, map, serializable } from 'serializr'; @@ -35,7 +37,7 @@ export namespace Field { * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string { - const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); + const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const valFunc = (field: FieldType): string => { const res = @@ -53,7 +55,7 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (onDelegate ? '=' : '') + valFunc(field); + return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(field); } export function toScriptString(field: FieldType) { switch (typeof field) { @@ -79,7 +81,7 @@ export namespace Field { // as a kind of macro to include the content of those documents Doc.MyPublishedDocs.forEach(doc => { const regexMultilineFlag = 'm'; - const regex = new RegExp(`^\\^${StrCast(doc.title).replace(/[\(\)]*/g, '')}\\s`, regexMultilineFlag); // need to remove characters that can cause the regular expression to be invalid + const regex = new RegExp(`^\\^${StrCast(doc.title).replace(/[()]*/g, '')}\\s`, regexMultilineFlag); // need to remove characters that can cause the regular expression to be invalid const sections = (Cast(doc.text, RichTextField, null)?.Text ?? '').split('--DOCDATA--'); if (script.match(regex)) { script = script.replace(regex, sections[0]) + (sections.length > 1 ? sections[1] : ''); @@ -148,19 +150,24 @@ export const ReverseHierarchyMap: Map key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); - if (Object.keys(permissions).length || doc[DocAcl]?.length) { - runInAction(() => (doc[DocAcl] = permissions)); - } + if (doc) { + const target = (doc as any)?.__fieldTuples ?? doc; + const permissions: { [key: string]: symbol } = !target.author || target.author === ClientUtils.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; + Object.keys(target).forEach(key => { + key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl); + }); + if (Object.keys(permissions).length || doc[DocAcl]?.length) { + runInAction(() => { + doc[DocAcl] = permissions; + }); + } - if (doc.proto instanceof Promise) { - doc.proto.then(proto => updateCachedAcls(DocCast(proto))); - return doc.proto; + if (doc.proto instanceof Promise) { + doc.proto.then(proto => updateCachedAcls(DocCast(proto))); + return doc.proto; + } } + return undefined; } @scriptingGlobal @@ -267,9 +274,9 @@ export class Doc extends RefField { return Reflect.getOwnPropertyDescriptor(target, prop); } return { - configurable: true, //TODO Should configurable be true? + configurable: true, // TODO Should configurable be true? enumerable: true, - value: 0, //() => target.__fieldTuples[prop]) + value: 0, // () => target.__fieldTuples[prop]) }; }, deleteProperty: deleteProperty, @@ -281,7 +288,8 @@ export class Doc extends RefField { if (!id || forceSave) { DocServer.CreateField(docProxy); } - return docProxy; + // eslint-disable-next-line no-constructor-return + return docProxy; // need to return the proxy from the constructor so that all our added fields will get called } [key: string]: FieldResult; @@ -293,6 +301,7 @@ export class Doc extends RefField { private set __fieldTuples(value) { // called by deserializer to set all fields in one shot this[FieldTuples] = value; + // eslint-disable-next-line no-restricted-syntax for (const key in value) { const field = value[key]; field !== undefined && (this[FieldKeys][key] = true); @@ -345,7 +354,7 @@ export class Doc extends RefField { let renderFieldKey: any; const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; if (typeof layoutField === 'string') { - renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1]; + [renderFieldKey] = layoutField.split("fieldKey={'")[1].split("'"); // layoutField.split("'")[1]; } else { return Cast(layoutField, Doc, null); } @@ -361,6 +370,7 @@ export class Doc extends RefField { for (const key in set) { const fprefix = 'fields.'; if (!key.startsWith(fprefix)) { + // eslint-disable-next-line no-continue continue; } const fKey = key.substring(fprefix.length); @@ -380,6 +390,7 @@ export class Doc extends RefField { const writeMode = DocServer.getFieldWriteMode(fKey); if (fKey.startsWith('acl') || writeMode !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; + // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; @@ -390,6 +401,7 @@ export class Doc extends RefField { if (unset) { for (const key in unset) { if (!key.startsWith('fields.')) { + // eslint-disable-next-line no-continue continue; } const fKey = key.substring(7); @@ -400,6 +412,7 @@ export class Doc extends RefField { }; if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; + // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; @@ -475,8 +488,8 @@ export namespace Doc { // 2) if the data doc has the field, then it's written there. // 3) if neither already has the field, then 'defaultProto' determines whether to write it to the data doc (or the embedding) // - export async function SetInPlace(doc: Doc, key: string, value: FieldType | undefined, defaultProto: boolean) { - if (key.startsWith('_')) key = key.substring(1); + export async function SetInPlace(doc: Doc, keyIn: string, value: FieldType | undefined, defaultProto: boolean) { + const key = keyIn.startsWith('_') ? keyIn.substring(1) : keyIn; const hasProto = doc[DocData] !== doc ? doc[DocData] : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; const onProto = hasProto && Object.getOwnPropertyNames(hasProto).indexOf(key) !== -1; @@ -561,7 +574,7 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) { - const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); + const key = fieldKey || Doc.LayoutFieldKey(listDoc); const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); @@ -578,7 +591,7 @@ export namespace Doc { * @returns true if successful, false otherwise. */ export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) { - const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); + const key = fieldKey || Doc.LayoutFieldKey(listDoc); const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { @@ -595,6 +608,7 @@ export namespace Doc { if (reversed) list.splice(0, 0, doc); else list.push(doc); } else { + // eslint-disable-next-line no-lonely-if if (reversed) list.splice(before ? list.length - ind + 1 : list.length - ind, 0, doc); else list.splice(before ? ind : ind + 1, 0, doc); } @@ -662,7 +676,9 @@ export namespace Doc { await Promise.all( Object.keys(doc).map(async key => { if (filter.includes(key)) return; - const assignKey = (val: any) => (copy[key] = val); + const assignKey = (val: any) => { + copy[key] = val; + }; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); const copyObjectField = async (field: ObjectField) => { @@ -714,7 +730,8 @@ export namespace Doc { } else if (field instanceof ObjectField) { await copyObjectField(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happen... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happen... } else { assignKey(field); } @@ -741,7 +758,7 @@ export namespace Doc { visited.add(clone); Object.keys(clone) .filter(key => key !== 'cloneOf') - .map(key => { + .forEach(key => { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { @@ -763,13 +780,13 @@ export namespace Doc { const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); const repaired = new Set(); const linkedDocs = Array.from(linkMap.values()); - linkedDocs.map(link => Doc.AddLink?.(link, true)); - rtfMap.map(({ copy, key, field }) => { - const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { + linkedDocs.forEach(link => Doc.AddLink?.(link, true)); + rtfMap.forEach(({ copy, key, field }) => { + const replacer = (match: any, attr: string, id: string /* , offset: any, string: any */) => { const mapped = cloneMap.get(id); return attr + '"' + (mapped ? mapped[Id] : id) + '"'; }; - const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => { + const replacer2 = (match: any, href: string, id: string /* , offset: any, string: any */) => { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; @@ -778,7 +795,7 @@ export namespace Doc { copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; - clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired)); + clonedDocs.forEach(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired)); return { clone: copy, map: cloneMap, linkMap }; } @@ -809,6 +826,7 @@ export namespace Doc { if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { + // eslint-disable-next-line no-param-reassign templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); @@ -879,6 +897,7 @@ export namespace Doc { } } } else if (cfield instanceof ComputedField) { + /* empty */ } else if (field instanceof ObjectField) { if (field instanceof Doc) { Doc.FindReferences(field, references, system); @@ -895,7 +914,8 @@ export namespace Doc { Doc.FindReferences(field.value, references, system); } } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happend... } } }); @@ -925,7 +945,8 @@ export namespace Doc { ? new ProxyField(Doc.MakeCopy(doc[key] as any)) // copy the expanded render template : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + // eslint-disable-next-line no-debugger + debugger; // This shouldn't happend... } else { copy[key] = field; } @@ -955,7 +976,9 @@ export namespace Doc { delegate.author = ClientUtils.CurrentUserEmail; Object.keys(doc) .filter(key => key.startsWith('acl')) - .forEach(key => (delegate[key] = doc[key])); + .forEach(key => { + delegate[key] = doc[key]; + }); title && (delegate.title = title); delegate[Initializing] = false; if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate); @@ -968,7 +991,7 @@ export namespace Doc { // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc). // This is appropriate if you're trying to create a document that behaves like all // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs) - export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string) { + export function MakeDelegateWithProto(doc: Doc /* , id?: string, title?: string */) { const ndoc = Doc.ApplyTemplate(doc); if (ndoc) { Doc.GetProto(ndoc).isDataDoc = true; @@ -1017,7 +1040,6 @@ export namespace Doc { !keepFieldKey && (templateField.title = metadataFieldKey); const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)]; - const templateCaptionValue = templateField.caption; // move any data that the template field had been rendering over to the template doc so that things will still be rendered // when the template field is adjusted to point to the new metadatafield key. // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well. @@ -1103,7 +1125,9 @@ export namespace Doc { return manager._searchQuery; } export function SetSearchQuery(query: string) { - runInAction(() => (manager._searchQuery = query)); + runInAction(() => { + manager._searchQuery = query; + }); } export function UserDoc(): Doc { return manager._user_doc; @@ -1115,12 +1139,13 @@ export namespace Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); } export function SetUserDoc(doc: Doc) { + // eslint-disable-next-line no-return-assign return (manager._user_doc = doc); } - const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(doc[DocData]) ? brushManager.SearchMatchDoc.get(doc[DocData]) : undefined; - }); + const isSearchMatchCache = computedFn((doc: Doc) => + (brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(doc[DocData]) ? brushManager.SearchMatchDoc.get(doc[DocData]) : undefined)); // prettier-ignore export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); } @@ -1166,10 +1191,14 @@ export namespace Doc { return BrushDoc(doc, true); } export function UnBrushAllDocs() { - Array.from(brushManager.BrushedDoc).forEach(action(doc => (doc[Brushed] = false))); + Array.from(brushManager.BrushedDoc).forEach( + action(doc => { + doc[Brushed] = false; + }) + ); } - let UnhighlightWatchers: (() => void)[] = []; + const UnhighlightWatchers: (() => void)[] = []; export let UnhighlightTimer: any; export function AddUnHighlightWatcher(watcher: () => void) { if (UnhighlightTimer) { @@ -1184,9 +1213,9 @@ export namespace Doc { highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc)); document.removeEventListener('pointerdown', linkFollowUnhighlight); } - export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentation_effect?: Doc) { + export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentationEffect?: Doc) { linkFollowUnhighlight(); - (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentation_effect)); + (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentationEffect)); document.removeEventListener('pointerdown', linkFollowUnhighlight); document.addEventListener('pointerdown', linkFollowUnhighlight); if (UnhighlightTimer) clearTimeout(UnhighlightTimer); @@ -1196,16 +1225,16 @@ export namespace Doc { }, 5000); } - export var highlightedDocs = new ObservableSet(); + export const highlightedDocs = new ObservableSet(); export function IsHighlighted(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(doc[DocData]) === AclPrivate || doc.opacity === 0) return false; return doc[Highlight] || doc[DocData][Highlight]; } - export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presentation_effect?: Doc) { + export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presentationEffect?: Doc) { runInAction(() => { highlightedDocs.add(doc); doc[Highlight] = true; - doc[Animation] = presentation_effect; + doc[Animation] = presentationEffect; if (dataAndDisplayDocs && !doc.resolvedDataDoc) { // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field highlightedDocs.add(doc[DocData]); @@ -1294,6 +1323,7 @@ export namespace Doc { const fields = childFilters[i].split(FilterSep); // split key:value:modifier if (fields[0] === key && (fields[1] === value.toString() || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { if (fields[2] === modifiers && modifiers && fields[1] === value.toString()) { + // eslint-disable-next-line no-param-reassign if (toggle) modifiers = 'remove'; else return; } @@ -1318,9 +1348,12 @@ export namespace Doc { return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; } } + return undefined; } export function assignDocToField(doc: Doc, field: string, id: string) { - DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout)); + DocServer.GetRefField(id).then(layout => { + layout instanceof Doc && (doc[field] = layout); + }); return id; } @@ -1395,6 +1428,7 @@ export namespace Doc { case DocumentType.EQUATION: return 'calculator'; case DocumentType.SIMULATION: return 'rocket'; case DocumentType.CONFIG: return 'folder-closed'; + default: } return 'question'; } @@ -1413,6 +1447,7 @@ export namespace Doc { const response = await fetch(upload, { method: 'POST', body: formData }); const json = await response.json(); if (json !== 'error') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const docs = await DocServer.GetRefFields(json.docids as string[]); const doc = DocCast(await DocServer.GetRefField(json.id)); const links = await DocServer.GetRefFields(json.linkids as string[]); @@ -1478,6 +1513,7 @@ export namespace Doc { */ export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt { if (excludeEmptyObjects === undefined) { + // eslint-disable-next-line no-param-reassign excludeEmptyObjects = true; } if (data === undefined || data === null || ![...primitives, 'object'].includes(typeof data)) { @@ -1517,12 +1553,12 @@ export namespace Doc { if (hasEntries || !excludeEmptyObjects) { const resolved = target ?? new Doc(); if (hasEntries) { - let result: Opt; - Object.keys(object).map(key => { + Object.keys(object).forEach(key => { // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have // otherwise been empty (List or Doc)s will just not be written - if ((result = toField(object[key], excludeEmptyObjects, key))) { + const result = toField(object[key], excludeEmptyObjects, key); + if (result) { resolved[key] = result; } }); @@ -1530,6 +1566,7 @@ export namespace Doc { title && (resolved.title = title); return resolved; } + return undefined; }; /** @@ -1545,10 +1582,13 @@ export namespace Doc { // if excludeEmptyObjects is true, any qualifying conversions from toField will // be undefined, and thus the results that would have // otherwise been empty (List or Doc)s will just not be written - list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result)); + list.forEach(item => { + (result = toField(item, excludeEmptyObjects)) && target.push(result); + }); if (target.length || !excludeEmptyObjects) { return target; } + return undefined; }; const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt => { @@ -1569,58 +1609,76 @@ export namespace Doc { export function IdToDoc(id: string) { return DocCast(DocServer.GetCachedRefField(id)); } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function idToDoc(id: string): any { return IdToDoc(id); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: any) { return StrCast(doc[DocData].title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getEmbedding(doc: any) { return Doc.MakeEmbedding(doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function docList(field: any) { return DocListCast(field); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added: Doc) { return Doc.AddDocToList(doc, field, added); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function docCastAsync(doc: FieldResult): any { return Cast(doc, Doc); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function activePresentationItem() { const curPres = Doc.ActivePresentation; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') { Doc.setDocFilter(container, key, value, modifiers); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function toJavascriptString(str: string) { return Field.toJavascriptString(str as FieldType); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function RtfField() { return RichTextField.RTFfield(); }); diff --git a/src/fields/util.ts b/src/fields/util.ts index f87c200c1..5ed10cf05 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -380,7 +380,7 @@ export function deleteProperty(target: any, prop: string | number | symbol) { // export function containedFieldChangedHandler(container: List | Doc, prop: string | number, liveContainedField: ObjectField) { let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField; - return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => { + return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: FieldType[] | undefined; length: number | undefined; hint?: any } /* , dummyServerOp?: any */) => { const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: FieldType) => SerializationHelper.Serialize(item)) }); // prettier-ignore const serverOp = diff?.op === '$addToSet' diff --git a/webpack.config.js b/webpack.config.js index 6ddd68d97..58df9a57d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,7 @@ const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // eslint-disable-next-line import/no-extraneous-dependencies -//const ESLintPlugin = require('eslint-webpack-plugin'); +// const ESLintPlugin = require('eslint-webpack-plugin'); // const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const { parsed } = require('dotenv').config(); -- cgit v1.2.3-70-g09d2 From 89e5b4e224d77c7a029ec7d9c9027095665508ac Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 19 Apr 2024 11:32:46 -0400 Subject: lint fixes. --- .gitignore | 1 - src/.DS_Store | Bin 10244 -> 10244 bytes src/ClientUtils.ts | 649 +++++++++++++++++++++ src/client/DocServer.ts | 10 +- src/client/documents/Documents.ts | 10 +- src/client/util/CurrentUserUtils.ts | 14 +- src/client/util/DocumentManager.ts | 2 +- src/client/util/GroupManager.tsx | 6 +- src/client/util/HypothesisUtils.ts | 2 +- src/client/util/SettingsManager.tsx | 4 +- src/client/util/SharingManager.tsx | 10 +- src/client/util/reportManager/ReportManager.tsx | 2 +- src/client/views/DashboardView.tsx | 6 +- src/client/views/MainView.tsx | 2 +- src/client/views/PropertiesView.tsx | 4 +- src/client/views/SidebarAnnos.tsx | 2 +- src/client/views/StyleProvider.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/collections/TreeView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 18 +- .../formattedText/FormattedTextBoxComment.tsx | 2 +- .../formattedText/ProsemirrorExampleTransfer.ts | 4 +- .../views/nodes/formattedText/RichTextRules.ts | 4 +- src/client/views/nodes/formattedText/marks_rts.ts | 2 +- src/fields/Doc.ts | 75 ++- src/fields/util.ts | 10 +- 27 files changed, 744 insertions(+), 103 deletions(-) create mode 100644 src/ClientUtils.ts (limited to 'src/client/util') diff --git a/.gitignore b/.gitignore index f841c1a86..6a1963b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules dist/ .DS_Store .env -ClientUtils.ts solr-8.3.1/server/logs/ solr-8.3.1/server/solr/dash/data/tlog/* solr-8.3.1/server/solr/dash/data/index/* diff --git a/src/.DS_Store b/src/.DS_Store index f8d745dbf..75cff7b55 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts new file mode 100644 index 000000000..57e0373c4 --- /dev/null +++ b/src/ClientUtils.ts @@ -0,0 +1,649 @@ +import * as Color from 'color'; +import * as React from 'react'; +import { ColorResult } from 'react-color'; +import * as rp from 'request-promise'; +import { numberRange, decimalToHexString } from './Utils'; +import { DocumentType } from './client/documents/DocumentTypes'; +import { Colors } from './client/views/global/globalEnums'; + +export function DashColor(color: string) { + try { + return color ? Color(color.toLowerCase()) : Color('transparent'); + } catch (e) { + if (color.includes('gradient')) console.log("using color 'white' in place of :" + color); + else console.log('COLOR error:', e); + return Color('white'); + } +} + +export function lightOrDark(color: any) { + if (color === 'transparent' || !color) return Colors.BLACK; + if (color.startsWith?.('linear')) return Colors.BLACK; + if (DashColor(color).isLight()) return Colors.BLACK; + return Colors.WHITE; +} + +export function returnTransparent() { + return 'transparent'; +} + +export function returnTrue() { + return true; +} + +export function returnIgnore(): 'ignore' { + return 'ignore'; +} +export function returnAlways(): 'always' { + return 'always'; +} +export function returnNever(): 'never' { + return 'never'; +} + +export function returnDefault(): 'default' { + return 'default'; +} + +export function return18() { + return 18; +} + +export function returnFalse() { + return false; +} + +export function returnAll(): 'all' { + return 'all'; +} + +export function returnNone(): 'none' { + return 'none'; +} + +export function returnVal(val1?: number, val2?: number) { + return val1 || (val2 !== undefined ? val2 : 0); +} + +export function returnOne() { + return 1; +} + +export function returnZero() { + return 0; +} + +export function returnEmptyString() { + return ''; +} + +export function returnEmptyFilter() { + return [] as string[]; +} + +export function returnEmptyDoclist() { + return [] as any[]; +} + +export namespace ClientUtils { + export const CLICK_TIME = 300; + export const DRAG_THRESHOLD = 4; + export const SNAP_THRESHOLD = 10; + let _currentUserEmail: string = ''; + export function CurrentUserEmail() { + return _currentUserEmail; + } + export function SetCurrentUserEmail(email: string) { + _currentUserEmail = email; + } + export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) { + return Date.now() - downTime < ClientUtils.CLICK_TIME && Math.abs(x - downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(y - downY) < ClientUtils.DRAG_THRESHOLD; + } + + export function cleanDocumentType(type: DocumentType) { + switch (type) { + case DocumentType.IMG: return 'Image'; + case DocumentType.AUDIO: return 'Audio'; + case DocumentType.COL: return 'Collection'; + case DocumentType.RTF: return 'Text'; + default: return type.charAt(0).toUpperCase() + type.slice(1); + } // prettier-ignore + } + + export function readUploadedFileAsText(inputFile: File) { + // eslint-disable-next-line no-undef + const temporaryFileReader = new FileReader(); + + return new Promise((resolve, reject) => { + temporaryFileReader.onerror = () => { + temporaryFileReader.abort(); + reject(new DOMException('Problem parsing input file.')); + }; + + temporaryFileReader.onload = () => { + resolve(temporaryFileReader.result); + }; + temporaryFileReader.readAsText(inputFile); + }); + } + + /** + * Uploads an image buffer to the server and stores with specified filename. by default the image + * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large) + * @param imageUri the bytes of the image + * @param returnedFilename the base filename to store the image on the server + * @param nosuffix optionally suppress creating multiple resolution images + */ + export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) { + try { + const posting = ClientUtils.prepend('/uploadURI'); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename, + nosuffix, + replaceRootFilename, + }, + json: true, + }); + return returnedUri; + } catch (e) { + console.log('ConvertDataURI :' + e); + } + return undefined; + } + + export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } { + if (!ele) { + return { scale: 1, translateX: 1, translateY: 1 }; + } + const rect = ele.getBoundingClientRect(); + const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth; + const translateX = rect.left; + const translateY = rect.top; + + return { scale, translateX, translateY }; + } + + /** + * A convenience method. Prepends the full path (i.e. http://localhost:) to the + * requested extension + * @param extension the specified sub-path to append to the window origin + */ + export function prepend(extension: string): string { + return window.location.origin + extension; + } + export function fileUrl(filename: string): string { + return prepend(`/files/${filename}`); + } + + export function shareUrl(documentId: string): string { + return prepend(`/doc/${documentId}?sharing=true`); + } + + export function CorsProxy(url: string): string { + return prepend('/corsProxy/') + encodeURIComponent(url); + } + + export function CopyText(text: string) { + navigator.clipboard.writeText(text); + } + + export function colorString(color: ColorResult) { + return color.hex.startsWith('#') && color.hex.length < 8 ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex; + } + + export function fromRGBAstr(rgba: string) { + const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/); + const r = rm ? Number(rm[1]) : 0; + const gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/); + const g = gm ? Number(gm[1]) : 0; + const bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/); + const b = bm ? Number(bm[1]) : 0; + const am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/); + const a = am ? Number(am[1]) : 1; + return { r: r, g: g, b: b, a: a }; + } + + const isTransparentFunctionHack = 'isTransparent(__value__)'; + export const noRecursionHack = '__noRecursion'; + + // special case filters + export const noDragDocsFilter = 'noDragDocs::any::check'; + export const TransparentBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + export const OpaqueBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + + export function IsRecursiveFilter(val: string) { + return !val.includes(noRecursionHack); + } + export function HasFunctionFilter(val: string) { + if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1; + // add other function filters here... + return undefined; + } + + export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) { + return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')'; + } + + export function HSLtoRGB(h: number, s: number, l: number) { + // Must be fractions of 1 + // s /= 100; + // l /= 100; + + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0; + let g = 0; + let b = 0; + if (h >= 0 && h < 60) { + r = c; + g = x; + b = 0; + } else if (h >= 60 && h < 120) { + r = x; + g = c; + b = 0; + } else if (h >= 120 && h < 180) { + r = 0; + g = c; + b = x; + } else if (h >= 180 && h < 240) { + r = 0; + g = x; + b = c; + } else if (h >= 240 && h < 300) { + r = x; + g = 0; + b = c; + } else if (h >= 300 && h < 360) { + r = c; + g = 0; + b = x; + } + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + return { r: r, g: g, b: b }; + } + + export function RGBToHSL(red: number, green: number, blue: number) { + // Make r, g, and b fractions of 1 + const r = red / 255; + const g = green / 255; + const b = blue / 255; + + // Find greatest and smallest channel values + const cmin = Math.min(r, g, b); + const cmax = Math.max(r, g, b); + const delta = cmax - cmin; + let h = 0; + let s = 0; + let l = 0; + // Calculate hue + + // No difference + if (delta === 0) h = 0; + // Red is max + else if (cmax === r) h = ((g - b) / delta) % 6; + // Green is max + else if (cmax === g) h = (b - r) / delta + 2; + // Blue is max + else h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + // Make negative hues positive behind 360° + if (h < 0) h += 360; // Calculate lightness + + l = (cmax + cmin) / 2; + + // Calculate saturation + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + + // Multiply l and s by 100 + // s = +(s * 100).toFixed(1); + // l = +(l * 100).toFixed(1); + + return { h: h, s: s, l: l }; + } + + export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) { + if (!targetHgt) return targetY; // if there's no height, then assume that + if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) { + return Math.ceil(targetY + minSpacing + targetHgt - contextHgt); + } + if (scrollTop >= Math.max(0, targetY - minSpacing)) { + return Math.max(0, Math.floor(targetY - minSpacing)); + } + return undefined; + } + + export function GetClipboardText(): string { + const textArea = document.createElement('textarea'); + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('paste'); + } catch (err) { + /* empty */ + } + + const val = textArea.value; + document.body.removeChild(textArea); + return val; + } +} + +export function OmitKeys(obj: any, keys: string[], pattern?: string, addKeyFunc?: (dup: any) => void): { omit: any; extract: any } { + const omit: any = { ...obj }; + const extract: any = {}; + keys.forEach(key => { + extract[key] = omit[key]; + delete omit[key]; + }); + pattern && + Array.from(Object.keys(omit)) + .filter(key => key.match(pattern)) + .forEach(key => { + extract[key] = omit[key]; + delete omit[key]; + }); + addKeyFunc?.(omit); + return { omit, extract }; +} + +export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) { + const dup: any = {}; + keys.forEach(key => { + dup[key] = obj[key]; + }); + addKeyFunc && addKeyFunc(dup); + return dup; +} + +export function incrementTitleCopy(title: string) { + const numstr = title.match(/.*(\{([0-9]*)\})+/); + const copyNumStr = `{${1 + (numstr ? +numstr[2] : 0)}}`; + return (numstr ? title.replace(numstr[1], '') : title) + copyNumStr; +} + +const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => { + if (transition === 'linear') { + const newCurrentTime = currentTime / duration; // currentTime / (duration / 2); + return start + newCurrentTime * change; + } + + let newCurrentTime = currentTime / (duration / 2); + if (newCurrentTime < 1) { + return (change / 2) * newCurrentTime * newCurrentTime + start; + } + + newCurrentTime -= 1; + return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; +}; + +export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) { + stopper?.(); + const elements = element instanceof HTMLElement ? [element] : element; + const starts = elements.map(element => element.scrollTop); + const startDate = new Date().getTime(); + let _stop = false; + const stop = () => { + _stop = true; + }; + const animateScroll = () => { + const currentDate = new Date().getTime(); + const currentTime = currentDate - startDate; + const setScrollTop = (element: HTMLElement, value: number) => { + element.scrollTop = value; + }; + if (!_stop) { + if (currentTime < duration) { + elements.forEach((element, i) => currentTime && setScrollTop(element, easeFunc(transition, Math.min(currentTime, duration), starts[i], to - starts[i], duration))); + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => setScrollTop(element, to)); + } + } + }; + animateScroll(); + return stop; +} + +export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) { + const elements = element instanceof HTMLElement ? [element] : element; + const starts = elements.map(element => element.scrollLeft); + const startDate = new Date().getTime(); + + const animateScroll = () => { + const currentDate = new Date().getTime(); + const currentTime = currentDate - startDate; + elements.forEach((element, i) => { + element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration); + }); + + if (currentTime < duration) { + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => { + element.scrollLeft = to; + }); + } + }; + animateScroll(); +} + +export function addStyleSheet(styleType: string = 'text/css') { + const style = document.createElement('style'); + style.type = styleType; + const sheets = document.head.appendChild(style); + return (sheets as any).sheet; +} +export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { + const propText = + typeof css === 'string' + ? css + : Object.keys(css) + .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p])) + .join(';'); + return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); +} +export function removeStyleSheetRule(sheet: any, rule: number) { + if (sheet.rules.length) { + sheet.removeRule(rule); + return true; + } + return false; +} +export function clearStyleSheetRules(sheet: any) { + if (sheet.rules.length) { + numberRange(sheet.rules.length).map(() => sheet.removeRule(0)); + return true; + } + return false; +} + +export function simulateMouseClick(element: Element | null | undefined, x: number, y: number, sx: number, sy: number, rightClick = true) { + if (!element) return; + ['pointerdown', 'pointerup'].forEach(event => { + const me = new PointerEvent(event, { + view: window, + bubbles: true, + cancelable: true, + button: 2, + pointerType: 'mouse', + clientX: x, + clientY: y, + screenX: sx, + screenY: sy, + }); + (me as any).dash = true; + element.dispatchEvent(me); + }); + + if (rightClick) { + const me = new MouseEvent('contextmenu', { + view: window, + bubbles: true, + cancelable: true, + button: 2, + clientX: x, + clientY: y, + movementX: 0, + movementY: 0, + screenX: sx, + screenY: sy, + }); + (me as any).dash = true; + element.dispatchEvent(me); + } +} + +export function getWordAtPoint(elem: any, x: number, y: number): string | undefined { + if (elem.tagName === 'INPUT') return 'input'; + if (elem.tagName === 'TEXTAREA') return 'textarea'; + if (elem.nodeType === elem.TEXT_NODE) { + const range = elem.ownerDocument.createRange(); + range.selectNodeContents(elem); + let currentPos = 0; + const endPos = range.endOffset; + while (currentPos + 1 <= endPos) { + range.setStart(elem, currentPos); + range.setEnd(elem, currentPos + 1); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) { + range.expand?.('word'); // doesn't exist in firefox + const ret = range.toString(); + range.detach(); + return ret; + } + currentPos += 1; + } + } else { + Array.from(elem.childNodes).forEach((childNode: any) => { + const range = childNode.ownerDocument.createRange(); + range.selectNodeContents(childNode); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) { + range.detach(); + const word = getWordAtPoint(childNode, x, y); + if (word) return word; + } else { + range.detach(); + } + return undefined; + }); + } + return undefined; +} + +export function isTargetChildOf(ele: HTMLDivElement | null, target: Element | null) { + let entered = false; + for (let child = target; !entered && child; child = child.parentElement) { + entered = child === ele; + } + return entered; +} + +export function StopEvent(e: React.PointerEvent | React.MouseEvent | React.KeyboardEvent) { + e.stopPropagation(); + e.preventDefault(); +} + +export function setupMoveUpEvents( + target: object, + e: React.PointerEvent, + moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean, + upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => any, + clickEvent: (e: PointerEvent, doubleTap?: boolean) => any, + // eslint-disable-next-line default-param-last + stopPropagation: boolean = true, + // eslint-disable-next-line default-param-last + stopMovePropagation: boolean = true, + noDoubleTapTimeout?: () => void +) { + const targetAny = target as any; + const doubleTapTimeout = 300; + targetAny._doubleTap = Date.now() - (target as any)._lastTap < doubleTapTimeout; + targetAny._lastTap = Date.now(); + targetAny._downX = targetAny._lastX = e.clientX; + targetAny._downY = targetAny._lastY = e.clientY; + targetAny._noClick = false; + let moving = false; + + const _moveEvent = (e: PointerEvent): void => { + if (moving || Math.abs(e.clientX - (target as any)._downX) > ClientUtils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > ClientUtils.DRAG_THRESHOLD) { + moving = true; + if ((target as any)._doubleTime) { + clearTimeout((target as any)._doubleTime); + targetAny._doubleTime = undefined; + } + if (moveEvent(e, [(target as any)._downX, (target as any)._downY], [e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) { + document.removeEventListener('pointermove', _moveEvent); + // eslint-disable-next-line no-use-before-define + document.removeEventListener('pointerup', _upEvent); + } + } + targetAny._lastX = e.clientX; + targetAny._lastY = e.clientY; + stopMovePropagation && e.stopPropagation(); + }; + const _upEvent = (e: PointerEvent): void => { + const isClick = Math.abs(e.clientX - targetAny._downX) < 4 && Math.abs(e.clientY - targetAny._downY) < 4; + upEvent(e, [e.clientX - targetAny._downX, e.clientY - targetAny._downY], isClick); + if (isClick) { + if (!targetAny._doubleTime && noDoubleTapTimeout) { + targetAny._doubleTime = setTimeout(() => { + noDoubleTapTimeout?.(); + targetAny._doubleTime = undefined; + }, doubleTapTimeout); + } + if (targetAny._doubleTime && targetAny._doubleTap) { + clearTimeout(targetAny._doubleTime); + targetAny._doubleTime = undefined; + } + targetAny._noClick = clickEvent(e, targetAny._doubleTap); + } + document.removeEventListener('pointermove', _moveEvent); + document.removeEventListener('pointerup', _upEvent, true); + }; + const _clickEvent = (e: MouseEvent): void => { + if ((target as any)._noClick) e.stopPropagation(); + document.removeEventListener('click', _clickEvent, true); + }; + if (stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + document.addEventListener('pointermove', _moveEvent); + document.addEventListener('pointerup', _upEvent, true); + document.addEventListener('click', _clickEvent, true); +} + +export function DivHeight(ele: HTMLElement): number { + return Number(getComputedStyle(ele).height.replace('px', '')); +} +export function DivWidth(ele: HTMLElement): number { + return Number(getComputedStyle(ele).width.replace('px', '')); +} + +export function dateRangeStrToDates(dateStr: string) { + // dateStr in yyyy-mm-dd format + const dateRangeParts = dateStr.split('|'); // splits into from and to date + const fromParts = dateRangeParts[0].split('-'); + const toParts = dateRangeParts[1].split('-'); + + const fromYear = parseInt(fromParts[0]); + const fromMonth = parseInt(fromParts[1]) - 1; + const fromDay = parseInt(fromParts[2]); + + const toYear = parseInt(toParts[0]); + const toMonth = parseInt(toParts[1]) - 1; + const toDay = parseInt(toParts[2]); + + return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)]; +} diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 804e6d1d7..b833d3287 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -102,7 +102,7 @@ export namespace DocServer { } export function getFieldWriteMode(field: string) { - return ClientUtils.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; + return ClientUtils.CurrentUserEmail() === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; } export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { @@ -463,7 +463,7 @@ export namespace DocServer { function _CreateFieldImpl(field: RefField) { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); } // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE @@ -481,7 +481,7 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); + !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); } function _respondToUpdateImpl(diff: any) { @@ -516,11 +516,11 @@ export namespace DocServer { } export function DeleteDocument(id: string) { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); } export function DeleteDocuments(ids: string[]) { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); } function _respondToDeleteImpl(ids: string | string[]) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d41a96db2..f827ea81c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -970,7 +970,7 @@ export namespace Docs { dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); dataProps.isSystem = viewProps.isSystem; dataProps.isDataDoc = true; - dataProps.author = ClientUtils.CurrentUserEmail; + dataProps.author = ClientUtils.CurrentUserEmail(); dataProps.author_date = new DateField(); if (fieldKey) { dataProps[`${fieldKey}_modificationDate`] = new DateField(); @@ -990,7 +990,7 @@ export namespace Docs { } if (!noView) { - const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail }; + const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail() }; viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default @@ -1330,7 +1330,7 @@ export namespace Docs { const doc = DockDocument( configs.map(c => c.doc), JSON.stringify(layoutConfig), - ClientUtils.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options }, + ClientUtils.CurrentUserEmail() === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options }, id ); configs.forEach(c => { @@ -1722,7 +1722,7 @@ export namespace DocUtils { event: undoable(() => { const newDoc = DocUtils.copyDragFactory(dragDoc); if (newDoc) { - newDoc.author = ClientUtils.CurrentUserEmail; + newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; @@ -1773,7 +1773,7 @@ export namespace DocUtils { event: undoable(() => { const newDoc = DocUtils.delegateDragFactory(dragDoc); if (newDoc) { - newDoc.author = ClientUtils.CurrentUserEmail; + newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 535f7cf9d..27ae5c9a0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -586,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: ClientUtils.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 }; @@ -832,8 +832,8 @@ pie title Minerals in my tap water static setupLinkDocs(doc: Doc, linkDatabaseId: string) { if (!(CurrentUserUtils.newAccount ? undefined : DocCast(doc.myLinkDatabase))) { const linkDocs = new Doc(linkDatabaseId, true); - linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail; - linkDocs.author = ClientUtils.CurrentUserEmail; + linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail(); + linkDocs.author = ClientUtils.CurrentUserEmail(); linkDocs.isSystem = true; linkDocs.data = new List([]); linkDocs["acl-Guest"] = SharingPermissions.Augment; @@ -893,11 +893,11 @@ pie title Minerals in my tap water 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(ClientUtils.CurrentUserEmail)) || []; + 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 = ClientUtils.CurrentUserEmail); + doc.title ?? (doc.title = ClientUtils.CurrentUserEmail()); Doc.noviceMode ?? (Doc.noviceMode = true); doc._showLabel ?? (doc._showLabel = true); doc.textAlign ?? (doc.textAlign = "left"); @@ -975,7 +975,7 @@ pie title Minerals in my tap water 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; }); - ClientUtils.CurrentUserEmail = result.email; + ClientUtils.SetCurrentUserEmail(result.email); resolvedPorts = result.resolvedPorts as any; DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email); if (result.cacheDocumentIds) @@ -1004,7 +1004,7 @@ pie title Minerals in my tap water const userDoc = CurrentUserUtils.newAccount ? new Doc(info.userDocumentId, true) : field as Doc; this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId); if (CurrentUserUtils.newAccount) { - if (ClientUtils.CurrentUserEmail === "guest") { + if (ClientUtils.CurrentUserEmail() === "guest") { DashboardView.createNewDashboard(undefined, "guest dashboard"); } else { userDoc.activePage = "home"; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 67a61f17e..9a7786125 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -321,7 +321,7 @@ export class DocumentManager { if (options.toggleTarget && (!options.didMove || docView.Document.hidden)) docView.Document.hidden = !docView.Document.hidden; if (options.effect) docView.Document[Animation] = options.effect; - if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && targetDoc.text_html) { + if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) { // if the docView is a text anchor, the contextView is the PDF/Web/Text doc contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect); DocumentManager._overlayViews.add(contextView); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 6b20b885b..c261c0f1e 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -136,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(ClientUtils.CurrentUserEmail) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail); + return accessList.includes(ClientUtils.CurrentUserEmail()) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail()); } /** @@ -148,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([ClientUtils.CurrentUserEmail]); + groupDoc.owners = JSON.stringify([ClientUtils.CurrentUserEmail()]); groupDoc.members = JSON.stringify(memberEmails); this.addGroup(groupDoc); } @@ -177,7 +177,7 @@ 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(ClientUtils.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); } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index d1600468a..48d3ac046 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -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 === ClientUtils.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/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index e2971895a..f983c29b7 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -128,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 ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); + } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); }); @undoBatch @@ -537,7 +537,7 @@ export class SettingsManager extends React.Component<{}> {
{DashVersion}
- {ClientUtils.CurrentUserEmail} + {ClientUtils.CurrentUserEmail()}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 03f7e9b71..ade4cc218 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -137,7 +137,7 @@ export class SharingManager extends React.Component<{}> { if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { this.populating = true; const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); runInAction(() => { FieldLoader.ServerLoadStatus.message = 'users'; }); @@ -234,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' ? ClientUtils.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); @@ -499,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(ClientUtils.CurrentUserEmail)}`; + const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; const curUserPermission = StrCast(targetDoc[userKey]); // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? (
- {targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)} + {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}
Owner
) : null, - sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? ( + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? (
Me
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index d6fd339a9..02b3ee32c 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -134,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, ClientUtils.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], }); diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 1d286c987..14abd5f89 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -64,7 +64,7 @@ export class DashboardView extends ObservableReactComponent<{}> { getDashboards = (whichGroup: DashboardGroup) => { if (whichGroup === DashboardGroup.MyDashboards) { - return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === ClientUtils.CurrentUserEmail); + return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === ClientUtils.CurrentUserEmail()); } return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); }; @@ -155,7 +155,7 @@ export class DashboardView extends ObservableReactComponent<{}> { : this.getDashboards(this.selectedDashboardGroup).map(dashboard => { const href = ImageCast(dashboard.thumb)?.url?.href; const shared = Object.keys(dashboard[DocAcl]) - .filter(key => key !== `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}` && !['acl-Me', 'acl-Guest'].includes(key)) + .filter(key => key !== `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}` && !['acl-Me', 'acl-Guest'].includes(key)) .some(key => dashboard[DocAcl][key] !== AclPrivate); return (
{ } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8a060d0e0..13945cacf 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -122,7 +122,7 @@ export class MainView extends ObservableReactComponent<{}> { } @observable mainDoc: Opt = undefined; @computed private get mainContainer() { - if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail === 'guest') { + if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail() === 'guest') { DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main => runInAction(() => { this.mainDoc = main as Doc; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 77e68866e..2a847e51a 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -405,7 +405,7 @@ export class PropertiesView extends ObservableReactComponent, props: Opt(moreProps?: X) { let ind; const doc = this.Document; const id = Doc.UserDoc()[Id]; - const email = ClientUtils.CurrentUserEmail; + const email = ClientUtils.CurrentUserEmail(); const pos = { x: position[0], y: position[1] }; if (id && email) { const proto = Doc.GetProto(doc); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index e266ccd14..20323a521 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -229,7 +229,7 @@ export class TreeView extends ObservableReactComponent { this.treeViewOpen = !this.treeViewOpen; } else { // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding - const bestEmbedding = docView.Document.author === ClientUtils.CurrentUserEmail && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document); + const bestEmbedding = docView.Document.author === ClientUtils.CurrentUserEmail() && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document); this._props.addDocTab(bestEmbedding, OpenWhere.lightbox); } }; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index dbd2ebe0d..62a3e2467 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1512,7 +1512,7 @@ export class DocumentView extends DocComponent LightboxView.Instance.SetLightboxDoc( diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c7543560d..a82f025f9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -580,7 +580,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const view = this._editorView!; - const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail }); + const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; protected createDropTarget = (ele: HTMLDivElement) => { @@ -722,7 +722,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } @@ -1489,7 +1489,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); @@ -1503,7 +1503,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; const { tr } = this._editorView.state; @@ -1534,7 +1534,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail()) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } @@ -1799,7 +1799,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type === schema.marks.user_mark && m.attrs.modified === modified); - _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified }))); + _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); } break; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 533fefa09..52d93ec38 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -10,7 +10,7 @@ import './FormattedTextBoxComment.scss'; import { schema } from './schema_rts'; export function findOtherUserMark(marks: readonly Mark[]): Mark | undefined { - return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail); + return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail()); } export function findUserMark(marks: readonly Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index e9ed2549e..c3a5a2c86 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -51,8 +51,8 @@ export function buildKeymap>(schema: S, props: any, mapKey case AclAugment: { const prevNode = state.selection.$cursor.nodeBefore; - const prevUser = !prevNode ? ClientUtils.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; - if (prevUser !== ClientUtils.CurrentUserEmail) { + const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks[prevNode.marks.length - 1].attrs.userid; + if (prevUser !== ClientUtils.CurrentUserEmail()) { return false; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 78ea99592..5b5617484 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -433,8 +433,8 @@ export class RichTextRules { return node ? state.tr .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 0f3306bef..d1c7b72a5 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -335,7 +335,7 @@ export const marks: { [index: string]: MarkSpec } = { const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); - const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail ? ' UM-remote' : ''; + const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail() ? ' UM-remote' : ''; return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0]; }, }, diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 3fb914423..f8351c238 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -152,7 +152,7 @@ export const ReverseHierarchyMap: Map { key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl); }); @@ -301,15 +301,16 @@ export class Doc extends RefField { private set __fieldTuples(value) { // called by deserializer to set all fields in one shot this[FieldTuples] = value; - // eslint-disable-next-line no-restricted-syntax - for (const key in value) { - const field = value[key]; - field !== undefined && (this[FieldKeys][key] = true); - if (field instanceof ObjectField) { - field[Parent] = this[Self]; - field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + Object.keys(value).forEach(key => { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const field = value[key]; + field !== undefined && (this[FieldKeys][key] = true); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + } } - } + }); } @observable private [FieldTuples]: any = {}; @@ -365,14 +366,11 @@ export class Doc extends RefField { public async [HandleUpdate](diff: any) { const set = diff.$set; - const sameAuthor = this.author === ClientUtils.CurrentUserEmail; - if (set) { - for (const key in set) { - const fprefix = 'fields.'; - if (!key.startsWith(fprefix)) { - // eslint-disable-next-line no-continue - continue; - } + const sameAuthor = this.author === ClientUtils.CurrentUserEmail(); + const fprefix = 'fields.'; + Object.keys(set ?? {}) + .filter(key => Object.prototype.hasOwnProperty.call(set, key) && key.startsWith(fprefix)) + .forEach(async key => { const fKey = key.substring(fprefix.length); const fn = async () => { const value = await SerializationHelper.Deserialize(set[key]); @@ -395,15 +393,11 @@ export class Doc extends RefField { } else { this[CachedUpdates][fKey] = fn; } - } - } + }); const unset = diff.$unset; - if (unset) { - for (const key in unset) { - if (!key.startsWith('fields.')) { - // eslint-disable-next-line no-continue - continue; - } + Object.keys(unset ?? {}) + .filter(key => Object.prototype.hasOwnProperty.call(unset, key) && key.startsWith(fprefix)) + .forEach(async key => { const fKey = key.substring(7); const fn = () => { this[UpdatingFromServer] = true; @@ -412,13 +406,11 @@ export class Doc extends RefField { }; if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; - // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; } - } - } + }); } } @@ -519,15 +511,15 @@ export namespace Doc { */ export function assign(doc: Doc, fields: Partial>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); - for (const key in fields) { - if (fields.hasOwnProperty(key)) { - const value = fields[key]; + Object.keys(fields).forEach(key => { + if (Object.prototype.hasOwnProperty.call(fields.hasOwnProperty, key)) { + const value = (fields as any)[key]; if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds? doc[key] = value; } } - } + }); isInitializing && (doc[Initializing] = false); return doc; } @@ -640,7 +632,7 @@ export namespace Doc { embedding.createdFrom = doc; embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); - embedding.author = ClientUtils.CurrentUserEmail; + embedding.author = ClientUtils.CurrentUserEmail(); return embedding; } @@ -648,7 +640,7 @@ export namespace Doc { export function BestEmbedding(doc: Doc) { const dataDoc = doc[DocData]; const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail); + const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -708,7 +700,7 @@ export namespace Doc { }; const docAtKey = doc[key]; if (key === 'author') { - assignKey(ClientUtils.CurrentUserEmail); + assignKey(ClientUtils.CurrentUserEmail()); } else if (docAtKey instanceof Doc) { if (pruneDocs.includes(docAtKey)) { // prune doc and do nothing @@ -717,7 +709,7 @@ export namespace Doc { (key.includes('layout[') || key.startsWith('layout') || // ['embedContainer', 'annotationOn', 'proto'].includes(key) || - (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail)) + (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) ) { assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } else { @@ -886,7 +878,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { if (field instanceof Doc) { if (key === 'myLinkDatabase') { @@ -934,7 +926,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { @@ -973,7 +965,7 @@ export namespace Doc { delegate[Initializing] = true; updateCachedAcls(delegate); delegate.proto = doc; - delegate.author = ClientUtils.CurrentUserEmail; + delegate.author = ClientUtils.CurrentUserEmail(); Object.keys(doc) .filter(key => key.startsWith('acl')) .forEach(key => { @@ -1004,7 +996,7 @@ export namespace Doc { export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { const proto = new Doc(); - proto.author = ClientUtils.CurrentUserEmail; + proto.author = ClientUtils.CurrentUserEmail(); const target = Doc.MakeDelegate(proto); const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); @@ -1199,7 +1191,8 @@ export namespace Doc { } const UnhighlightWatchers: (() => void)[] = []; - export let UnhighlightTimer: any; + let UnhighlightTimer: any; + export function IsUnhighlightTimerSet() { return UnhighlightTimer; } // prettier-ignore export function AddUnHighlightWatcher(watcher: () => void) { if (UnhighlightTimer) { UnhighlightWatchers.push(watcher); diff --git a/src/fields/util.ts b/src/fields/util.ts index 5ed10cf05..d7268f31a 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -84,7 +84,7 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail; + const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail(); const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField); const writeToServer = @@ -163,7 +163,7 @@ const getEffectiveAclCache = computedFn((target: any, user?: string) => getEffec */ export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; - if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail === 'guest') return AclAdmin; + if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail() === 'guest') return AclAdmin; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } @@ -186,7 +186,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; - const userChecked = user || ClientUtils.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + const userChecked = user || ClientUtils.CurrentUserEmail(); // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; Object.entries(targetAcls).forEach(([key, value]) => { @@ -219,7 +219,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { */ // eslint-disable-next-line default-param-last export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) { - const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; + const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; if (!target || visited.includes(target) || key === selfKey) return; visited.push(target); @@ -268,7 +268,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { - [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])] + [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail() !== parent.author ? ['acl-Owner'] : [])] .filter(key => key.startsWith('acl')) .forEach(key => { // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private. -- cgit v1.2.3-70-g09d2 From 939e18624af4252551f38c43335ee8ef0acd144c Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 21 Apr 2024 19:03:49 -0400 Subject: more lint cleanup --- .eslintrc.json | 1 + package-lock.json | 3 +- package.json | 2 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 82 ++- src/client/util/BranchingTrailManager.tsx | 41 +- src/client/util/CalendarManager.tsx | 39 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/util/DragManager.ts | 18 +- src/client/util/GroupManager.tsx | 85 ++- src/client/util/HypothesisUtils.ts | 2 - src/client/util/Import & Export/ImageUtils.ts | 2 +- src/client/util/LinkFollower.ts | 7 +- src/client/util/LinkManager.ts | 28 +- src/client/util/ScriptingGlobals.ts | 1 + src/client/util/SearchUtil.ts | 20 +- src/client/util/SettingsManager.tsx | 323 +++++---- src/client/util/SharingManager.tsx | 753 +++++++++++---------- src/client/util/SnappingManager.ts | 3 + src/client/util/UndoManager.ts | 90 +-- src/client/util/reportManager/ReportManager.tsx | 38 +- src/client/views/ContextMenuItem.tsx | 15 +- src/client/views/DashboardView.tsx | 41 +- src/client/views/DictationOverlay.tsx | 55 +- src/client/views/DocComponent.tsx | 5 +- src/client/views/DocumentButtonBar.tsx | 114 ++-- src/client/views/EditableView.tsx | 23 +- src/client/views/FieldsDropdown.tsx | 16 +- src/client/views/FilterPanel.tsx | 1 + src/client/views/GlobalKeyHandler.ts | 58 +- src/client/views/InkingStroke.tsx | 109 +-- src/client/views/MainView.tsx | 42 +- src/client/views/MetadataEntryMenu.tsx | 196 ------ src/client/views/OverlayView.tsx | 7 +- src/client/views/PropertiesButtons.tsx | 182 +++-- src/client/views/PropertiesView.tsx | 44 +- src/client/views/SidebarAnnos.tsx | 13 +- src/client/views/StyleProvider.tsx | 75 +- src/client/views/UndoStack.tsx | 19 +- src/client/views/animationtimeline/Timeline.tsx | 1 + src/client/views/animationtimeline/Track.tsx | 2 +- .../collections/CollectionMasonryViewFieldRow.tsx | 96 +-- src/client/views/collections/CollectionMenu.tsx | 19 +- .../views/collections/CollectionNoteTakingView.tsx | 85 ++- .../views/collections/CollectionPileView.tsx | 13 +- .../collections/CollectionStackedTimeline.tsx | 152 +++-- src/client/views/collections/CollectionSubView.tsx | 31 +- .../views/collections/CollectionTimeView.tsx | 32 +- .../views/collections/CollectionTreeView.tsx | 79 ++- src/client/views/collections/TabDocView.tsx | 347 +++++----- src/client/views/collections/TreeView.tsx | 46 +- .../CollectionFreeFormInfoUI.tsx | 33 +- .../CollectionFreeFormLayoutEngines.tsx | 38 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 295 +++++--- .../collections/collectionFreeForm/MarqueeView.tsx | 10 +- .../collectionSchema/SchemaTableCell.tsx | 33 +- src/client/views/global/globalScripts.ts | 13 +- src/client/views/linking/LinkMenuItem.tsx | 32 +- src/client/views/linking/LinkPopup.tsx | 21 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 44 +- src/client/views/nodes/ComparisonBox.tsx | 17 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 115 ++-- .../nodes/DataVizBox/components/Histogram.tsx | 230 +++---- .../nodes/DataVizBox/components/LineChart.tsx | 107 ++- .../views/nodes/DataVizBox/components/TableBox.tsx | 80 ++- src/client/views/nodes/DocumentContentsView.tsx | 33 +- src/client/views/nodes/DocumentLinksButton.tsx | 34 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FieldView.tsx | 10 +- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 60 +- src/client/views/nodes/ImageBox.tsx | 7 +- src/client/views/nodes/KeyValuePair.tsx | 13 +- src/client/views/nodes/LabelBox.tsx | 25 +- src/client/views/nodes/LinkAnchorBox.tsx | 6 +- src/client/views/nodes/MapBox/MapBox.tsx | 25 +- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 31 +- src/client/views/nodes/PDFBox.tsx | 100 ++- .../views/nodes/RecordingBox/RecordingBox.tsx | 37 +- src/client/views/nodes/ScreenshotBox.tsx | 29 +- src/client/views/nodes/VideoBox.tsx | 103 ++- src/client/views/nodes/WebBox.tsx | 153 +++-- .../views/nodes/formattedText/DashFieldView.tsx | 310 +++++---- .../nodes/formattedText/FormattedTextBox.scss | 3 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../formattedText/ProsemirrorExampleTransfer.ts | 51 +- .../views/nodes/formattedText/RichTextMenu.tsx | 226 +++---- .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/client/views/nodes/formattedText/marks_rts.ts | 30 +- src/client/views/nodes/formattedText/nodes_rts.ts | 14 +- src/client/views/nodes/trails/PresBox.tsx | 635 ++++++++++------- src/client/views/nodes/trails/PresElementBox.tsx | 96 +-- src/client/views/pdf/Annotation.tsx | 4 +- src/client/views/pdf/GPTPopup/GPTPopup.tsx | 3 +- src/client/views/search/SearchBox.tsx | 68 +- src/client/views/topbar/TopBar.tsx | 25 +- src/fields/Doc.ts | 56 +- src/fields/List.ts | 34 +- src/fields/ObjectField.ts | 1 + src/fields/Proxy.ts | 17 +- src/fields/Schema.ts | 2 +- src/fields/Types.ts | 15 +- src/fields/documentSchemas.ts | 3 +- src/mobile/ImageUpload.tsx | 77 +-- src/mobile/MobileInterface.tsx | 2 +- src/server/ApiManagers/UploadManager.ts | 4 +- 104 files changed, 3740 insertions(+), 3134 deletions(-) delete mode 100644 src/client/views/MetadataEntryMenu.tsx (limited to 'src/client/util') diff --git a/.eslintrc.json b/.eslintrc.json index 0c4e375a9..780626412 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,7 @@ "react/destructuring-assignment": 0, "no-restricted-globals": ["error", "event"], "no-param-reassign": ["error", { "props": false }], + "import/no-cycle": 0, "no-alert": 0, "radix": "off" }, diff --git a/package-lock.json b/package-lock.json index cc1a0ea64..c616bb6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,6 +183,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", @@ -287,7 +288,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -30110,7 +30110,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz", "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==", - "dev": true, "peerDependencies": { "prop-types": "^15.5.4", "react": ">= 15.0.0", diff --git a/package.json b/package.json index 3f2f5a70f..a3ba6bdd9 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -267,6 +266,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 757031fec..07a2708ec 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-use-before-define */ +import Photos = require('googlephotos'); import { AssertionError } from 'assert'; import { EditorState } from 'prosemirror-state'; import { ClientUtils } from '../../../ClientUtils'; @@ -5,14 +7,12 @@ import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { RichTextUtils } from '../../../fields/RichTextUtils'; -import { Cast, StrCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; +import { Cast, ImageCast, StrCast } from '../../../fields/Types'; import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes'; import { Networking } from '../../Network'; import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents'; import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox'; import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager'; -import Photos = require('googlephotos'); export namespace GooglePhotos { const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); @@ -76,17 +76,16 @@ export namespace GooglePhotos { export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise> => { const { collection, title, descriptionKey, tag } = options; const dataDocument = Doc.GetProto(collection); - const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data)); if (!images || !images.length) { return undefined; } - const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`; + const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`; const { id, productUrl } = await Create.Album(resolved); const response = await Transactions.UploadImages(images, { id }, descriptionKey); if (response) { const { results, failed } = response; - let index: Opt; - while ((index = failed.pop()) !== undefined) { + for (let index = failed.pop(); index !== undefined; index = failed.pop()) { Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]); } const mediaItems: MediaItem[] = results.map(item => item.mediaItem); @@ -97,13 +96,12 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; - if (!mediaItem) { - continue; + if (mediaItem) { + image.googlePhotosId = mediaItem.id; + image.googlePhotosAlbumUrl = productUrl; + image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; + idMapping[mediaItem.id] = image; } - image.googlePhotosId = mediaItem.id; - image.googlePhotosAlbumUrl = productUrl; - image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; - idMapping[mediaItem.id] = image; } collection.googlePhotosAlbumUrl = productUrl; collection.googlePhotosIdMapping = idMapping; @@ -114,6 +112,7 @@ export namespace GooglePhotos { Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); return { albumId: id, mediaItems }; } + return undefined; }; } @@ -124,7 +123,7 @@ export namespace GooglePhotos { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const response = await Query.ContentSearch(requested); const uploads = await Transactions.WriteMediaItemsToServer(response); - const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/)); + const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */)); const options = { _width: 500, _height: 500 }; return constructor(children, options); }; @@ -144,7 +143,7 @@ export namespace GooglePhotos { const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); - for (const value of values) { + values.forEach(async value => { const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id); searched?.forEach(async id => { const image = await Cast(idMapping[id], Doc); @@ -154,7 +153,7 @@ export namespace GooglePhotos { !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); } }); - } + }); images?.forEach(image => { const concatenated = tagMapping.get(image[Id])!; const tags = concatenated.split(delimiter); @@ -200,9 +199,10 @@ export namespace GooglePhotos { export const AlbumSearch = async (albumId: string, pageSize = 100): Promise => { const photos = await endpoint(); const mediaItems: MediaItem[] = []; - let nextPageTokenStored: Opt = undefined; + let nextPageTokenStored: Opt; const found = 0; do { + // eslint-disable-next-line no-await-in-loop const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored); mediaItems.push(...response.mediaItems); nextPageTokenStored = response.nextPageToken; @@ -222,7 +222,7 @@ export namespace GooglePhotos { excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); filters.setContentFilter(contentFilter); - const date = options.date; + const { date } = options; if (date) { const dateFilter = new photos.DateFilter(); if (date instanceof Date) { @@ -240,15 +240,11 @@ export namespace GooglePhotos { }); }; - export const GetImage = async (mediaItemId: string): Promise => { - return (await endpoint()).mediaItems.get(mediaItemId); - }; + export const GetImage = async (mediaItemId: string): Promise => (await endpoint()).mediaItems.get(mediaItemId); } namespace Create { - export const Album = async (title: string) => { - return (await endpoint()).albums.create(title); - }; + export const Album = async (title: string) => (await endpoint()).albums.create(title); } export namespace Transactions { @@ -278,6 +274,7 @@ export namespace GooglePhotos { return enrichmentItem.id; } } + return undefined; }; export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise => { @@ -291,9 +288,12 @@ export namespace GooglePhotos { return undefined; } const baseUrls: string[] = await Promise.all( - response.results.map(item => { - return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); - }) + response.results.map( + item => + new Promise(resolve => { + Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)); + }) + ) ); return baseUrls; }; @@ -303,27 +303,25 @@ export namespace GooglePhotos { failed: number[]; } - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise> => { + export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - if (album && 'title' in album) { - album = await Create.Album(album.title); - } + const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn; const media: MediaInput[] = []; - for (const source of sources) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; - } - const url = data.url.href; - const target = Doc.MakeEmbedding(source); - const description = parseDescription(target, descriptionKey); - await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); - media.push({ url, description }); - } + sources + .filter(source => ImageCast(Doc.GetProto(source).data)) + .forEach(async source => { + const data = ImageCast(Doc.GetProto(source).data); + const url = data.url.href; + const target = Doc.MakeEmbedding(source); + const description = parseDescription(target, descriptionKey); + await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); + media.push({ url, description }); + }); if (media.length) { const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album }); return results; } + return undefined; }; const parseDescription = (document: Doc, descriptionKey: string) => { diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx index 02879e3c4..28c00644f 100644 --- a/src/client/util/BranchingTrailManager.tsx +++ b/src/client/util/BranchingTrailManager.tsx @@ -1,18 +1,31 @@ +/* eslint-disable react/no-unused-class-component-methods */ +/* eslint-disable react/no-array-index-key */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { PresBox } from '../views/nodes/trails'; import { OverlayView } from '../views/OverlayView'; +import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; -import { Docs } from '../documents/Documents'; -import { nullAudio } from '../../fields/URLField'; @observer export class BranchingTrailManager extends React.Component { + // eslint-disable-next-line no-use-before-define public static Instance: BranchingTrailManager; + // stack of the history + @observable private slideHistoryStack: String[] = []; + @observable private containsSet: Set = new Set(); + // docId to Doc map + @observable private docIdToDocMap: Map = new Map(); + + // prev pres to copmare with + @observable private prevPresId: String | null = null; + @action setPrevPres = action((newId: String | null) => { + this.prevPresId = newId; + }); + constructor(props: any) { super(props); makeObservable(this); @@ -22,7 +35,7 @@ export class BranchingTrailManager extends React.Component { } setupUi = () => { - OverlayView.Instance.addWindow(, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); + OverlayView.Instance.addWindow(, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); // OverlayView.Instance.forceUpdate(); console.log(OverlayView.Instance); // let hi = Docs.Create.TextDocument("beee", { @@ -36,23 +49,11 @@ export class BranchingTrailManager extends React.Component { console.log(DocumentManager._overlayViews); }; - // stack of the history - @observable private slideHistoryStack: String[] = []; @action setSlideHistoryStack = action((newArr: String[]) => { this.slideHistoryStack = newArr; }); - @observable private containsSet: Set = new Set(); - - // prev pres to copmare with - @observable private prevPresId: String | null = null; - @action setPrevPres = action((newId: String | null) => { - this.prevPresId = newId; - }); - - // docId to Doc map - @observable private docIdToDocMap: Map = new Map(); - + // eslint-disable-next-line react/sort-comp observeDocumentChange = (targetDoc: Doc, pres: PresBox) => { const presId = pres.Document[Id]; if (this.prevPresId === presId) { @@ -106,7 +107,7 @@ export class BranchingTrailManager extends React.Component { if (this.slideHistoryStack.length === 0) { Doc.UserDoc().isBranchingMode = false; } - //PresBox.NavigateToTarget(targetDoc, targetDoc); + // PresBox.NavigateToTarget(targetDoc, targetDoc); }; @computed get trailBreadcrumbs() { @@ -116,11 +117,11 @@ export class BranchingTrailManager extends React.Component { const [presId, targetDocId] = info.split(','); const doc = this.docIdToDocMap.get(targetDocId); if (!doc) { - return <>; + return null; } return ( - -{'>'} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 6e9094b3a..46aa4d238 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -1,4 +1,10 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; +import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { TextField } from '@mui/material'; +import { Button } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -6,21 +12,16 @@ import Select from 'react-select'; import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { StrCast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './CalendarManager.scss'; import { DocumentManager } from './DocumentManager'; import { SelectionManager } from './SelectionManager'; import { SettingsManager } from './SettingsManager'; -// import { DateRange, Range, RangeKeyDict } from 'react-date-range'; -import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; -import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button } from 'browndash-components'; -import { Docs } from '../documents/Documents'; -import { ObservableReactComponent } from '../views/ObservableReactComponent'; // import 'react-date-range/dist/styles.css'; // import 'react-date-range/dist/theme/default.css'; @@ -47,6 +48,7 @@ const formatCalendarDateToString = (calendarDate: any) => { @observer export class CalendarManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: CalendarManager; @observable private isOpen = false; @observable private targetDoc: Doc | undefined = undefined; // the target document @@ -83,10 +85,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { this.creationType = type; }; - public open = (target?: DocumentView, target_doc?: Doc) => { + public open = (target?: DocumentView, targetDoc?: Doc) => { console.log('hi'); runInAction(() => { - this.targetDoc = target_doc || target?.Document; + this.targetDoc = targetDoc || target?.Document; this.targetDocView = target; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; @@ -117,7 +119,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action handleSelectChange = (option: any) => { if (option) { - let selectOpt = option as CalendarSelectOptions; + const selectOpt = option as CalendarSelectOptions; this.selectedExistingCalendarOption = selectOpt; this.calendarName = selectOpt.value; // or label } @@ -136,7 +138,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { // TODO: Make undoable private addToCalendar = () => { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar console.log(targetDoc); @@ -159,7 +161,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { } } else { // find existing calendar based on selected name (should technically always find one) - const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName); + const existingCalendar = this.existingCalendars.find(findCal => StrCast(findCal.title) === this.calendarName); if (existingCalendar) calendar = existingCalendar; else { this.errorMessage = 'Must select an existing calendar'; @@ -252,11 +254,9 @@ export class CalendarManager extends ObservableReactComponent<{}> { @computed get calendarInterface() { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - const currentDate = new Date(); - return (
{ {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}

-
this.setInterationType('new-calendar')}> +
this.setInterationType('new-calendar')}> Add to New Calendar
-
this.setInterationType('existing-calendar')}> +
this.setInterationType('existing-calendar')}> Add to Existing calendar
@@ -317,7 +317,8 @@ export class CalendarManager extends ObservableReactComponent<{}> { color: StrCast(Doc.UserDoc().userColor), width: '100%', }), - }}> + }} + /> )}
@@ -351,6 +352,6 @@ export class CalendarManager extends ObservableReactComponent<{}> { } render() { - return ; + return ; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 27ae5c9a0..6dba8027d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -709,7 +709,7 @@ pie title Minerals in my tap water return [ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(); }' }}, { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(); }'}}, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, ]; } static videoTools() { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 62f055f1a..3890b7845 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -495,18 +495,20 @@ 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('*'))] + .map(dele => (dele as any).style) + .forEach(style => { + style && (style.pointerEvents = 'none'); + }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { - const children = [Array.from(ele.children), Array.from(dragElement.children)]; - while (children[0].length) { - const childs = [children[0].pop(), children[1].pop()]; + const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)]; + while (dragChildren[0].length) { + const childs = [dragChildren[0].pop(), dragChildren[1].pop()]; if (childs[0]?.children) { - children[0].push(...Array.from(childs[0].children)); - children[1].push(...Array.from(childs[1]!.children)); + dragChildren[0].push(...Array.from(childs[0].children)); + dragChildren[1].push(...Array.from(childs[1]!.children)); } if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index c261c0f1e..8d84dbad8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -30,6 +32,7 @@ export interface UserOptions { @observer export class GroupManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @observable private users: string[] = []; // list of users populated from the database. @@ -160,7 +163,7 @@ export class GroupManager extends ObservableReactComponent<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); - this.GroupManagerDoc['data_modificationDate'] = new DateField(); + this.GroupManagerDoc.data_modificationDate = new DateField(); return true; } return false; @@ -202,7 +205,7 @@ export class GroupManager extends ObservableReactComponent<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } @@ -216,10 +219,9 @@ export class GroupManager extends ObservableReactComponent<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { - const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } } @@ -275,7 +277,9 @@ export class GroupManager extends ObservableReactComponent<{}> { TaskCompletionBox.textDisplayed = 'Group created!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); }; @@ -292,7 +296,7 @@ export class GroupManager extends ObservableReactComponent<{}> {

- (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} /> + { + this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'; + })} + />
this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> + {this.sharingOptions(uniform)} + + ) : ( +
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} +   +
+ )} +
+
+ ); }); - const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); - raw.map( - action((newUser: User) => { - const sharingDoc = docs[newUser.sharingDocumentId]; - const linkDatabase = docs[newUser.linkDatabaseId]; - if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { - if (!this.users.find(user => user.user.email === newUser.email)) { - this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - //LinkManager.addLinkDB(linkDatabase); - } - } - }) + + // checks if every doc has the same author + const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); + + // the owner of the doc and the current user are placed at the top of the user list. + const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; + const curUserPermission = StrCast(targetDoc[userKey]); + // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name + userListContents.unshift( + sameAuthor ? ( +
+ {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)} +
+
Owner
+
+
+ ) : null, + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( +
+ Me +
+
+ {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} +   +
+
+
+ ) : null + ); + + // the list of groups shared with + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); + groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" }); + const groupListContents = groupListMap.map(group => { + const groupKey = `acl-${StrCast(group.title)}`; + const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); + const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; + + return !permissions ? null : ( +
+
{StrCast(group.title)}
+   + {group instanceof Doc ? ( + } + size={Size.XSMALL} + color={SettingsManager.userColor} + onClick={action(() => { + GroupManager.Instance.currentGroup = group; + })} + /> + ) : null} +
+ {admin || this.myDocAcls ? ( + + ) : ( +
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} +   +
+ )} +
+
); - this.populating = false; - } - }; + }); + return ( +
+ {GroupManager.Instance?.currentGroup ? ( + { + GroupManager.Instance.currentGroup = undefined; + })} + /> + ) : null} +
+

+

window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> + window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> +
+ Share + {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} +

+
+
+
+
+ {admin ? ( +
+
+ + {this.sharingOptions(true)} + +
+
+
+
+
+ { + this.showUserOptions = !this.showUserOptions; + })} + />{' '} + + { + this.showGroupOptions = !this.showGroupOptions; + })} + />{' '} + +
+ +
+ {Doc.noviceMode ? null : ( +
+ { + this.upgradeNested = !this.upgradeNested; + })} + checked={this.upgradeNested} + />{' '} + + { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + +
+ )} +
+
+ ) : ( +
+
+
+ { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + +
+
+
+ )} +
+
+
{ + this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'; + })}> +
+ Individuals + } + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + /> +
+
+
{userListContents}
+
+
+
{ + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> +
+ Groups + } size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> + } + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + /> +
+
+
{groupListContents}
+
+
+
+
+ ); + } /** * Shares the document with a user. @@ -200,12 +479,69 @@ export class SharingManager extends React.Component<{}> { } }); }, 'set group permissions'); + /** + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. + */ + populateUsers = async () => { + if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { + this.populating = true; + 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) => { + const sharingDoc = docs[newUser.sharingDocumentId]; + const linkDatabase = docs[newUser.linkDatabaseId]; + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + if (!this.users.find(user => user.user.email === newUser.email)) { + this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); + // LinkManager.addLinkDB(linkDatabase); + } + } + }) + ); + this.populating = false; + } + }; + + // eslint-disable-next-line react/sort-comp + public close = action(() => { + this.isOpen = false; + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + // this.copied = false; + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + // eslint-disable-next-line react/no-unused-class-component-methods + public open = (target?: DocumentView, targetDoc?: Doc) => { + this.populateUsers(); + runInAction(() => { + this.targetDocView = target; + this.targetDoc = targetDoc || target?.Document; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Augment; + this.upgradeNested = true; + }); + }; /** * Shares the documents shared with a group with a new user who has been added to that group. * @param group * @param emailId */ + // eslint-disable-next-line react/no-unused-class-component-methods shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { const user = this.users.find(({ user: { email } }) => email === emailId)!; const self = this; @@ -231,6 +567,7 @@ export class SharingManager extends React.Component<{}> { /** * Called from the properties sidebar to change permissions of a user. */ + // eslint-disable-next-line react/no-unused-class-component-methods shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { if (layout) this.layoutDocAcls = true; if (shareWith !== 'Guest') { @@ -254,6 +591,7 @@ export class SharingManager extends React.Component<{}> { * @param group * @param emailId */ + // eslint-disable-next-line react/no-unused-class-component-methods removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; @@ -277,6 +615,7 @@ export class SharingManager extends React.Component<{}> { * Removes a group's permissions from documents that have been shared with it. * @param group */ + // eslint-disable-next-line react/no-unused-class-component-methods removeGroup = (group: Doc) => { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { @@ -299,25 +638,12 @@ export class SharingManager extends React.Component<{}> { // targetDoc["acl-" + PublicKey] = permission; // }s - /** - * Copies the Public sharing url to the user's clipboard. - */ - private copyURL = (e: any) => { - ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); - }; - - /** - * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share - */ - private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { - const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift('-multiple-'); - return dropdownValues.map(permission => ( - - )); - } + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = () => { + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); + }; private focusOn = (contents: string) => { const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; @@ -350,24 +676,6 @@ export class SharingManager extends React.Component<{}> { ); }; - /** - * Handles changes in the users selected in react-select - */ - @action - handleUsersChange = (selectedOptions: any) => { - this.selectedUsers = selectedOptions as UserOptions[]; - }; - - /** - * Handles changes in the permission chosen to share with someone with - */ - handlePermissionsChange = undoable( - action((event: React.ChangeEvent) => { - this.permissions = event.currentTarget.value as SharingPermissions; - }), - 'permission change' - ); - /** * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. */ @@ -389,7 +697,9 @@ export class SharingManager extends React.Component<{}> { TaskCompletionBox.textDisplayed = 'Document shared!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); } @@ -418,263 +728,20 @@ export class SharingManager extends React.Component<{}> { const g2 = StrCast(group2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - /** - * @returns the main interface of the SharingManager. + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share */ - @computed get sharingInterface() { - if (!this.targetDoc) return null; - TraceMobx(); - const groupList = GroupManager.Instance?.allGroups || []; - - const sortedUsers = this.users - .slice() - .sort(this.sortUsers) - .map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList - .slice() - .sort(this.sortGroups) - .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); - - // the next block handles the users shown (individuals/groups/both) - const options: GroupedOptions[] = []; - if (GroupManager.Instance) { - if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { - options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups }); - } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers }); - else options.push({ label: 'Groups', options: sortedGroups }); - } - - const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users; - const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList; - - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); - - if (this.myDocAcls) { - const newDocs: Doc[] = []; - SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc)); - docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); - } - - const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - - // tslint:disable-next-line: no-unnecessary-callback-wrapper - const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); - const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); - - // users in common between all docs - const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]); - - // the list of users shared with - const userListContents = users - // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) - .filter(({ user }) => docs[0]?.author !== user.email) - .map(({ user, linkDatabase, sharingDoc, userColor }) => { - const userKey = `acl-${normalizeEmail(user.email)}`; - const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]); - // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; - let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]); - permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; - - return !permissions ? null : ( -
- {user.email} -
- {admin || this.myDocAcls ? ( - - ) : ( -
- {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} -   -
- )} -
-
- ); - }); - - // checks if every doc has the same author - const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); - - // the owner of the doc and the current user are placed at the top of the user list. - const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; - const curUserPermission = StrCast(targetDoc[userKey]); - // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name - userListContents.unshift( - sameAuthor ? ( -
- {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)} -
-
Owner
-
-
- ) : null, - sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( -
- Me -
-
- {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} -   -
-
-
- ) : null - ); - - // the list of groups shared with - const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); - groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); - const groupListContents = groupListMap.map(group => { - let groupKey = `acl-${StrCast(group.title)}`; - const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); - const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; - - return !permissions ? null : ( -
-
{StrCast(group.title)}
-   - {group instanceof Doc ? } size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} -
- {admin || this.myDocAcls ? ( - - ) : ( -
- {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} -   -
- )} -
-
- ); - }); - return ( -
- {GroupManager.Instance?.currentGroup ? (GroupManager.Instance.currentGroup = undefined))} /> : null} -
-

-

window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> - window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> -
- Share - {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} -

-
-
-
-
- {admin ? ( -
-
- - {this.sharingOptions(true)} - -
-
-
-
-
- (this.showUserOptions = !this.showUserOptions))} /> - (this.showGroupOptions = !this.showGroupOptions))} /> -
- -
- {Doc.noviceMode ? null : ( -
- (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> - (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> -
- )} -
-
- ) : ( -
-
-
- (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> -
-
-
- )} -
-
-
(this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> -
- Individuals - } - size={Size.XSMALL} - color={StrCast(Doc.UserDoc().userColor)} - /> -
-
-
{userListContents}
-
-
-
(this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> -
- Groups - } size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> - } - size={Size.XSMALL} - color={StrCast(Doc.UserDoc().userColor)} - /> -
-
-
{groupListContents}
-
-
-
-
- ); + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift('-multiple-'); + return dropdownValues.map(permission => ( + + )); } render() { - return ; + return ; } } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index eb47bbe88..3da85191f 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -10,6 +10,7 @@ export class SnappingManager { @observable _shiftKey = false; @observable _ctrlKey = false; @observable _metaKey = false; + @observable _showPresPaths = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized @@ -36,6 +37,7 @@ export class SnappingManager { public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore + public static get ShowPresPaths() { return this.Instance._showPresPaths; } // prettier-ignore public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore public static get IsDragging() { return this.Instance._isDragging; } // prettier-ignore public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore @@ -44,6 +46,7 @@ export class SnappingManager { 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 SetShowPresPaths = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // 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 diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 4e941508d..956c0e674 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,8 +1,11 @@ +/* eslint-disable prefer-spread */ +/* eslint-disable no-use-before-define */ import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; import { RichTextField } from '../../fields/RichTextField'; -export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen +// eslint-disable-next-line prefer-const +let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -38,10 +41,11 @@ function propertyDecorator(target: any, key: string | symbol) { } export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { - return function () { + return function (...fargs) { const batch = UndoManager.StartBatch(batchName); try { - return fn.apply(undefined, arguments as any); + // eslint-disable-next-line prefer-rest-params + return fn.apply(undefined, fargs); } finally { batch.end(); } @@ -49,13 +53,15 @@ export function undoable(fn: (...args: any[]) => any, batchName: string): (...ar } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor): any; +// eslint-disable-next-line no-redeclare export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; +// eslint-disable-next-line no-redeclare export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor): any { if (!key) { - return function () { + return function (...fargs: any[]) { const batch = UndoManager.StartBatch(''); try { - return target.apply(undefined, arguments); + return target.apply(undefined, fargs); } finally { batch.end(); } @@ -63,7 +69,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed } if (!descriptor) { propertyDecorator(target, key); - return; + return undefined; } const oldFunction = descriptor.value; @@ -87,14 +93,18 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - export let undoStackNames: string[] = observable([]); - export let redoStackNames: string[] = observable([]); - export let undoStack: UndoBatch[] = observable([]); - export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - export let batchCounter = observable.box(0); let undoing = false; - export let tempEvents: UndoEvent[] | undefined = undefined; + let tempEvents: UndoEvent[] | undefined; + export const undoStackNames: string[] = observable([]); + export const redoStackNames: string[] = observable([]); + export const undoStack: UndoBatch[] = observable([]); + export const redoStack: UndoBatch[] = observable([]); + export const batchCounter = observable.box(0); + let _fieldPrinter: (val: any) => string = val => val?.toString(); + export function SetFieldPrinter(printer: (val: any) => string) { + _fieldPrinter = printer; + } export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { @@ -103,8 +113,8 @@ export namespace UndoManager { ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + - ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) + ' = ' + // prettier-ignore + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value)) ); currentBatch.push(event); tempEvents?.push(event); @@ -130,21 +140,22 @@ export namespace UndoManager { } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; - const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); + const lastStack = UndoManager.undoStack.slice(-1)[0]; // .lastElement(); if (lastStack) { - lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); + lastStack.forEach(ev => { + fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1); + }); const fieldCount2: { [key: string]: number } = {}; - runInAction( - () => - (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { - if (fieldTypes.includes(ev.prop)) { - fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; - if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; - return false; - } - return true; - })) - ); + runInAction(() => { + UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + }); + }); } } export function TraceOpenBatches() { @@ -161,11 +172,10 @@ export namespace UndoManager { if (this.disposed) { console.log('WARNING: undo batch already disposed'); return false; - } else { - this.disposed = true; - openBatches.splice(openBatches.indexOf(this)); - return EndBatch(this.batchName, cancel); } + this.disposed = true; + openBatches.splice(openBatches.indexOf(this)); + return EndBatch(this.batchName, cancel); }; end = () => this.dispose(false); @@ -183,7 +193,7 @@ export namespace UndoManager { const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); - printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); @@ -200,10 +210,10 @@ export namespace UndoManager { export function StartTempBatch() { tempEvents = []; } - export function EndTempBatch(success: boolean) { + export function EndTempBatch(success: boolean) { UndoManager.UndoTempBatch(success); } - //TODO Make this return the return value + // TODO Make this return the return value export function RunInBatch(fn: () => T, batchName: string) { const batch = StartBatch(batchName); try { @@ -235,9 +245,11 @@ export namespace UndoManager { } undoing = true; - for (let i = commands.length - 1; i >= 0; i--) { - commands[i].undo(); - } + // eslint-disable-next-line prettier/prettier + commands + .slice() + .reverse() + .forEach(command => command.undo()); undoing = false; redoStackNames.push(names ?? '???'); @@ -256,9 +268,7 @@ export namespace UndoManager { } undoing = true; - for (const command of commands) { - command.redo(); - } + commands.forEach(command => command.redo()); undoing = false; undoStackNames.push(names ?? '???'); diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 02b3ee32c..2224e642d 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/media-has-caption */ +/* eslint-disable react/no-unused-class-component-methods */ import { Octokit } from '@octokit/core'; import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -13,7 +16,7 @@ import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { MainViewModal } from '../../views/MainViewModal'; -import '.././SettingsManager.scss'; +import '../SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; import './ReportManager.scss'; import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; @@ -25,10 +28,12 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d */ @observer export class ReportManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: ReportManager; @observable private isOpen = false; @observable private query = ''; + // eslint-disable-next-line react/sort-comp @action private setQuery = (q: string) => { this.query = q; }; @@ -83,7 +88,9 @@ export class ReportManager extends React.Component<{}> { this.formData = newData; }); - public close = action(() => (this.isOpen = false)); + public close = action(() => { + this.isOpen = false; + }); public open = action(async () => { this.isOpen = true; if (this.shownIssues.length === 0) { @@ -165,7 +172,7 @@ export class ReportManager extends React.Component<{}> { * @returns JSX element of a piece of media (image, video, audio) */ private getMediaPreview = (fileData: FileData): JSX.Element => { - const file = fileData.file; + const { file } = fileData; const mimeType = file.type; const preview = URL.createObjectURL(file); @@ -180,7 +187,8 @@ export class ReportManager extends React.Component<{}> {
); - } else if (mimeType.startsWith('video/')) { + } + if (mimeType.startsWith('video/')) { return (
@@ -194,7 +202,8 @@ export class ReportManager extends React.Component<{}> {
); - } else if (mimeType.startsWith('audio/')) { + } + if (mimeType.startsWith('audio/')) { return (
); } - return <>; + return
; }; /** @@ -307,8 +316,8 @@ export class ReportManager extends React.Component<{}> {
{ @@ -320,8 +329,8 @@ export class ReportManager extends React.Component<{}> { /> { @@ -347,7 +356,7 @@ export class ReportManager extends React.Component<{}> { text="Submit" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} - icon={} + icon={} iconPlacement="right" onClick={() => { this.reportIssue(); @@ -364,7 +373,7 @@ export class ReportManager extends React.Component<{}> { /> )}
- } onClick={this.close} /> + } onClick={this.close} />
); @@ -376,9 +385,8 @@ export class ReportManager extends React.Component<{}> { private reportComponent = () => { if (this.viewState === ViewState.VIEW) { return this.viewIssuesComponent(); - } else { - return this.reportIssueComponent(); } + return this.reportIssueComponent(); }; render() { @@ -386,7 +394,7 @@ export class ReportManager extends React.Component<{}> { diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 5760872fb..eb1030eec 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable, runInAction } from 'mobx'; @@ -17,6 +18,7 @@ export interface OriginalMenuProps { export interface SubmenuProps { description: string; + // eslint-disable-next-line no-use-before-define subitems: ContextMenuProps[]; noexpand?: boolean; addDivider?: boolean; @@ -37,7 +39,9 @@ export class ContextMenuItem extends ObservableReactComponent (this._items.length = 0)); + runInAction(() => { + this._items.length = 0; + }); if ((this._props as SubmenuProps)?.subitems) { (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); } @@ -83,7 +87,9 @@ export class ContextMenuItem extends ObservableReactComponent (this.overItem = false)), + action(() => { + this.overItem = false; + }), ContextMenuItem.timeout ); }; @@ -147,10 +153,10 @@ export class ContextMenuItem extends ObservableReactComponent {this._props.description} - +
); } + return null; } } diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 14abd5f89..25415a4f0 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -48,10 +50,18 @@ export class DashboardView extends ObservableReactComponent<{}> { @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; @observable private newDashboardName = ''; @observable private newDashboardColor = '#AFAFAF'; - @action abortCreateNewDashboard = () => (this.openModal = false); - @action setNewDashboardName = (name: string) => (this.newDashboardName = name); - @action setNewDashboardColor = (color: string) => (this.newDashboardColor = color); - @action selectDashboardGroup = (group: DashboardGroup) => (this.selectedDashboardGroup = group); + @action abortCreateNewDashboard = () => { + this.openModal = false; + }; + @action setNewDashboardName = (name: string) => { + this.newDashboardName = name; + }; + @action setNewDashboardColor = (color: string) => { + this.newDashboardColor = color; + }; + @action selectDashboardGroup = (group: DashboardGroup) => { + this.selectedDashboardGroup = group; + }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { @@ -138,9 +148,9 @@ export class DashboardView extends ObservableReactComponent<{}> { <>
-
open linked trail
}>
-
- +
); @@ -134,8 +144,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))} - onPointerLeave={action(e => (this.subEndLink = ''))} + onPointerEnter={action(() => { + this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : ''); + })} + onPointerLeave={action(() => { + this.subEndLink = ''; + })} onClick={e => { this.view0 && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.Document, true, this.view0, { @@ -157,7 +171,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( {linkBtn(false, true, 'address-card')} {linkBtn(true, true, 'id-card')}
- +
); } @@ -177,15 +191,16 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action( - e => - (this.subPin = - (pinLayoutView ? 'Layout' : '') + - (pinLayoutView && pinContentView ? ' &' : '') + - (pinContentView ? ' Content View' : '') + - (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : '')) - )} - onPointerLeave={action(e => (this.subPin = ''))} + onPointerEnter={action(() => { + this.subPin = + (pinLayoutView ? 'Layout' : '') + + (pinLayoutView && pinContentView ? ' &' : '') + + (pinContentView ? ' Content View' : '') + + (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''); + })} + onPointerLeave={action(() => { + this.subPin = ''; + })} onClick={e => { const docs = this._props .views() @@ -232,8 +247,8 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get shareButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - {'Open Sharing Manager'}
}> -
SharingManager.Instance.open(this.view0, targetDoc)}> + Open Sharing Manager
}> +
SharingManager.Instance.open(this.view0, targetDoc)}>
@@ -244,7 +259,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get menuButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - {`Open Context Menu`}
}> + Open Context Menu
}>
setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => this.openContextMenu(e))}>
@@ -260,8 +275,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{ - console.log('hi: ', CalendarManager.Instance); + onClick={() => { CalendarManager.Instance.open(this.view0, targetDoc); }}> @@ -282,7 +296,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }} onPointerDown={action((e: React.PointerEvent) => { this._isRecording = true; - this._props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction)); + this._props.views().map( + view => + view && + DocumentViewInternal.recordAudioAnnotation( + view.dataDoc, + view.LayoutFieldKey, + stopFunc => { + this._stopFunc = stopFunc; + }, + emptyFunction + ) + ); const b = UndoManager.StartBatch('Recording'); setupMoveUpEvents( this, @@ -310,10 +335,10 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.Document]); - const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); + const origin = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); dragData.defaultDropAction = dropActionType.embed; dragData.canEmbed = true; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, origin[0], origin[1], { hideSource: false }); return true; } return false; @@ -336,8 +361,19 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @computed get templateButton() { return !this.view0 ? null : ( - Tap to Customize Layout. Drag an embedding
} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> -
!this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> + Tap to Customize Layout. Drag an embedding
} + open={this._tooltipOpen} + onClose={action(() => { + this._tooltipOpen = false; + })} + placement="bottom"> +
{ + !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true); + })}> } popup={this.templateMenu} popupContainsPt={returnTrue} />
@@ -365,17 +401,17 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @observable _captureEndLinkLayout = false; @action - captureEndLinkLayout = (e: React.PointerEvent) => { + captureEndLinkLayout = () => { this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @observable _captureEndLinkContent = false; @action - captureEndLinkContent = (e: React.PointerEvent) => { + captureEndLinkContent = () => { this._captureEndLinkContent = !this._captureEndLinkContent; }; @action - captureEndLinkState = (e: React.PointerEvent) => { + captureEndLinkState = () => { this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @@ -402,13 +438,15 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( return (
- +
{this._showLinkPopup ? (
(link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick))} + linkCreated={link => { + link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick); + }} linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this._props.views().lastElement()?.Document} /> @@ -423,7 +461,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{this.pinButton}
{this.recordButton}
{this.calendarButton}
- {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null :
{this.shareButton}
} + {!Doc.UserDoc().documentLinksButton_fullMenu ? null :
{this.shareButton}
}
{this.menuButton}
); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 85e893e19..684b948af 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -70,7 +72,7 @@ export class EditableView extends ObservableReactComponent { constructor(props: EditableProps) { super(props); makeObservable(this); - this._editing = this._props.editing ? true : false; + this._editing = !!this._props.editing; } componentDidMount(): void { @@ -166,7 +168,7 @@ export class EditableView extends ObservableReactComponent { this._props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; } - + // eslint-disable-next-line no-fallthrough default: if (this._props.textCallback?.(e.key)) { e.stopPropagation(); @@ -186,7 +188,6 @@ export class EditableView extends ObservableReactComponent { this._editing = true; this._props.isEditingCallback?.(true); } - // e.stopPropagation(); } }; @@ -223,6 +224,7 @@ export class EditableView extends ObservableReactComponent { renderEditor() { return this._props.autosuggestProps ? ( { ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? ( (this._inputref = r)} + ref={r => { this._inputref = r; }} // prettier-ignore style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }} placeholder={this._props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this._props.GetValue()} - autoFocus={true} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus onChange={this.onChange} onKeyDown={this.onKeyDown} onPointerDown={this.stopPropagation} @@ -256,12 +259,13 @@ export class EditableView extends ObservableReactComponent { ) : (