diff options
Diffstat (limited to 'src/client/util')
40 files changed, 1445 insertions, 1163 deletions
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx index a224b84f4..02879e3c4 100644 --- a/src/client/util/BranchingTrailManager.tsx +++ b/src/client/util/BranchingTrailManager.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; @@ -15,13 +15,14 @@ export class BranchingTrailManager extends React.Component { constructor(props: any) { super(props); + makeObservable(this); if (!BranchingTrailManager.Instance) { BranchingTrailManager.Instance = this; } } setupUi = () => { - OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail'}); + OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); // OverlayView.Instance.forceUpdate(); console.log(OverlayView.Instance); // let hi = Docs.Create.TextDocument("beee", { @@ -30,11 +31,10 @@ export class BranchingTrailManager extends React.Component { // }) // hi.overlayX = 100; // hi.overlayY = 100; - + // Doc.AddToMyOverlay(hi); console.log(DocumentManager._overlayViews); }; - // stack of the history @observable private slideHistoryStack: String[] = []; @@ -54,7 +54,7 @@ export class BranchingTrailManager extends React.Component { @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>(); observeDocumentChange = (targetDoc: Doc, pres: PresBox) => { - const presId = pres.props.Document[Id]; + const presId = pres.Document[Id]; if (this.prevPresId === presId) { return; } @@ -69,7 +69,7 @@ export class BranchingTrailManager extends React.Component { if (this.prevPresId === null || this.prevPresId !== presId) { Doc.UserDoc().isBranchingMode = true; this.setPrevPres(presId); - + // REVERT THE SET const stringified = [presId, targetDocId].toString(); if (this.containsSet.has([presId, targetDocId].toString())) { @@ -98,7 +98,7 @@ export class BranchingTrailManager extends React.Component { const newStack = this.slideHistoryStack.slice(0, removeIndex); const removed = this.slideHistoryStack.slice(removeIndex); - + this.setSlideHistoryStack(newStack); removed.forEach(info => this.containsSet.delete(info.toString())); diff --git a/src/client/util/CalendarManager.scss b/src/client/util/CalendarManager.scss new file mode 100644 index 000000000..114e19a0e --- /dev/null +++ b/src/client/util/CalendarManager.scss @@ -0,0 +1,62 @@ +.calendar-interface{ + width: 600px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 7px; + padding: 10px; + + .selected-doc-title{ + font-size: 1.4em; + } + + .creation-type-container{ + display: flex; + justify-content: center; + align-items: center; + align-self: center; + gap: 12px; + + .calendar-creation{ + cursor: pointer; + } + + .calendar-creation-selected{ + border-bottom: 2px solid white; + } + } + + .choose-calendar-container{ + margin-top: 10px; + align-self: center; + width: 60%; + } + + .description-container{ + margin-top: 10px; + align-self: center; + width: 60%; + } + + .date-range-picker-container{ + margin-top: 5px; + align-self: center; + display: flex; + flex-direction: column; + gap: 2px; + } + + .create-button-container{ + + margin-top: 5px; + align-self: center; + + .button-content{ + font-size: 1.2em; + padding: 10px; + border-radius: 5px; + background-color: #EFF2F7; + } + } +} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx new file mode 100644 index 000000000..6e9094b3a --- /dev/null +++ b/src/client/util/CalendarManager.tsx @@ -0,0 +1,356 @@ +import { TextField } from '@mui/material'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import Select from 'react-select'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; +import { StrCast } from '../../fields/Types'; +import { DictationOverlay } from '../views/DictationOverlay'; +import { MainViewModal } from '../views/MainViewModal'; +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'; + +type CreationType = 'new-calendar' | 'existing-calendar' | 'manage-calendars'; + +interface CalendarSelectOptions { + label: string; + value: string; +} + +const formatCalendarDateToString = (calendarDate: any) => { + console.log('Formatting the following date: ', calendarDate); + const date = new Date(calendarDate.year, calendarDate.month - 1, calendarDate.day); + console.log(typeof date); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; + +// TODO: If doc is already part of a calendar, display that +// TODO: For a doc already in a calendar: give option to edit date range, delete from calendar + +@observer +export class CalendarManager extends ObservableReactComponent<{}> { + public static Instance: CalendarManager; + @observable private isOpen = false; + @observable private targetDoc: Doc | undefined = undefined; // the target document + @observable private targetDocView: DocumentView | undefined = undefined; // the DocumentView of the target doc + @observable private dialogueBoxOpacity = 1; // for the modal + @observable private overlayOpacity = 0.4; // for the modal + + @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used + + @observable private creationType: CreationType = 'new-calendar'; + + @observable private existingCalendars: Doc[] = DocListCast(Doc.MyCalendars?.data); + + @computed get selectOptions() { + return this.existingCalendars.map(calendar => ({ label: StrCast(calendar.title), value: StrCast(calendar.title) })); + } + + @observable + selectedExistingCalendarOption: CalendarSelectOptions | null = null; + + @observable + calendarName: string = ''; + + @observable + calendarDescription: string = ''; + + @observable + errorMessage: string = ''; + + @action + setInterationType = (type: CreationType) => { + this.errorMessage = ''; + this.calendarName = ''; + this.creationType = type; + }; + + public open = (target?: DocumentView, target_doc?: Doc) => { + console.log('hi'); + runInAction(() => { + this.targetDoc = target_doc || target?.Document; + this.targetDocView = target; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + }); + }; + + public close = action(() => { + this.isOpen = false; + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + constructor(props: {}) { + super(props); + CalendarManager.Instance = this; + makeObservable(this); + } + + componentDidMount(): void {} + + @action + handleSelectChange = (option: any) => { + if (option) { + let selectOpt = option as CalendarSelectOptions; + this.selectedExistingCalendarOption = selectOpt; + this.calendarName = selectOpt.value; // or label + } + }; + + @action + handleCalendarTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + console.log('Existing calendars: ', this.existingCalendars); + this.calendarName = event.target.value; + }; + + @action + handleCalendarDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + this.calendarDescription = event.target.value; + }; + + // TODO: Make undoable + private addToCalendar = () => { + let 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); + if (targetDoc) { + let calendar: Doc; + if (this.creationType === 'new-calendar') { + if (!this.existingCalendars.find(doc => StrCast(doc.title) === this.calendarName)) { + console.log('creating...'); + calendar = Docs.Create.CalendarDocument( + { + title: this.calendarName, + description: this.calendarDescription, + }, + [] + ); + console.log('successful calendar creation'); + } else { + this.errorMessage = 'Calendar with this name already exists'; + return; + } + } else { + // find existing calendar based on selected name (should technically always find one) + const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName); + if (existingCalendar) calendar = existingCalendar; + else { + this.errorMessage = 'Must select an existing calendar'; + return; + } + } + // Get start and end date strings + const startDateStr = formatCalendarDateToString(this.selectedDateRange.start); + const endDateStr = formatCalendarDateToString(this.selectedDateRange.end); + + console.log('start date: ', startDateStr); + console.log('end date: ', endDateStr); + + const subDocEmbedding = Doc.MakeEmbedding(targetDoc); // embedding + console.log('subdoc embedding', subDocEmbedding); + subDocEmbedding.embedContainer = calendar; // set embed container + subDocEmbedding.date_range = `${startDateStr}|${endDateStr}`; // set subDoc date range + + Doc.AddDocToList(calendar, 'data', subDocEmbedding); // add embedded subDoc to calendar + + console.log('my calendars: ', Doc.MyCalendars); + if (this.creationType === 'new-calendar') { + Doc.AddDocToList(Doc.MyCalendars, 'data', calendar); // add to new calendar to dashboard calendars + } + } + }; + + private focusOn = (contents: string) => { + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; + const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.Document) : [this.targetDoc]; + return ( + <span + className="focus-span" + title={title} + onClick={() => { + if (this.targetDoc && this.targetDocView && docs.length === 1) { + DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true }); + } + }} + onPointerEnter={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.BrushDoc(doc)); + this.dialogueBoxOpacity = 0.1; + this.overlayOpacity = 0.1; + } + })} + onPointerLeave={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); + this.dialogueBoxOpacity = 1; + this.overlayOpacity = 0.4; + } + })}> + {contents} + </span> + ); + }; + + @observable + selectedDateRange: any = [ + { + start: new Date(), + end: new Date(), + }, + ]; + + @action + setSelectedDateRange = (range: any) => { + console.log('Range: ', range); + this.selectedDateRange = range; + }; + + @computed + get createButtonActive() { + if (this.calendarName.length === 0 || this.errorMessage.length > 0) return false; // disabled if no calendar name + let startDate: Date | undefined; + let endDate: Date | undefined; + try { + startDate = this.selectedDateRange.start; + endDate = this.selectedDateRange.end; + console.log(startDate); + console.log(endDate); + } catch (e: any) { + console.log(e); + return false; // disabled + } + if (!startDate || !endDate) return false; // disabled if any is undefined + return true; + } + + @computed + get calendarInterface() { + let 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 ( + <div + className="calendar-interface" + style={{ + background: SettingsManager.userBackgroundColor, + color: StrCast(Doc.UserDoc().userColor), + }}> + <p className="selected-doc-title" style={{ color: SettingsManager.userColor }}> + <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b> + </p> + <div className="creation-type-container"> + <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('new-calendar')}> + Add to New Calendar + </div> + <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('existing-calendar')}> + Add to Existing calendar + </div> + </div> + <div className="choose-calendar-container"> + {this.creationType === 'new-calendar' ? ( + <TextField + fullWidth + onChange={this.handleCalendarTitleChange} + label="Calendar name" + placeholder="Enter a name..." + variant="filled" + style={{ + backgroundColor: 'white', + color: 'black', + borderRadius: '5px', + }} + /> + ) : ( + <Select + className="existing-calendar-search" + placeholder="Search for existing calendar..." + isClearable + isSearchable + options={this.selectOptions} + value={this.selectedExistingCalendarOption} + onChange={this.handleSelectChange} + styles={{ + control: () => ({ + display: 'inline-flex', + width: '100%', + }), + indicatorSeparator: () => ({ + display: 'inline-flex', + visibility: 'hidden', + }), + indicatorsContainer: () => ({ + display: 'inline-flex', + textDecorationColor: 'black', + }), + valueContainer: () => ({ + display: 'inline-flex', + fontStyle: StrCast(Doc.UserDoc().userColor), + color: StrCast(Doc.UserDoc().userColor), + width: '100%', + }), + }}></Select> + )} + </div> + <div className="description-container"> + <TextField + fullWidth + multiline + label="Calendar description" + placeholder="Enter a description (optional)..." + onChange={this.handleCalendarDescriptionChange} + variant="filled" + style={{ + backgroundColor: 'white', + color: 'black', + borderRadius: '5px', + }} + /> + </div> + <div className="date-range-picker-container"> + <div>Select a date range: </div> + <Provider theme={defaultTheme}> + <DateRangePicker aria-label="Select a date range" value={this.selectedDateRange} onChange={v => this.setSelectedDateRange(v)} /> + </Provider> + </div> + {this.createButtonActive && ( + <div className="create-button-container"> + <Button onClick={() => this.addToCalendar()} text="Add to Calendar" iconPlacement="right" icon={<FontAwesomeIcon icon={faPlus as IconLookup} />} /> + </div> + )} + </div> + ); + } + + render() { + return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + } +} diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss index 11e31fe2e..8679a0101 100644 --- a/src/client/util/CaptureManager.scss +++ b/src/client/util/CaptureManager.scss @@ -1,4 +1,4 @@ -@import "../views/global/globalCssVariables"; +@import '../views/global/globalCssVariables.module'; .capture-interface { //background-color: whitesmoke !important; @@ -39,7 +39,7 @@ align-items: center; justify-content: center; } - + .recordButtonInner { border-radius: 100%; width: 70%; @@ -106,7 +106,7 @@ display: flex; justify-content: center; align-items: center; - background-color: #BDDBE8; + background-color: #bddbe8; border-radius: 100%; font-weight: 800; margin-right: 5px; @@ -154,4 +154,3 @@ } } } - diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx index f42336ee7..2e13aff2f 100644 --- a/src/client/util/CaptureManager.tsx +++ b/src/client/util/CaptureManager.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; @@ -15,11 +15,12 @@ import { SelectionManager } from './SelectionManager'; export class CaptureManager extends React.Component<{}> { public static Instance: CaptureManager; static _settingsStyle = addStyleSheet(); - @observable _document: any; + @observable _document: any = undefined; @observable isOpen: boolean = false; // whether the CaptureManager is to be displayed or not. constructor(props: {}) { super(props); + makeObservable(this); CaptureManager.Instance = this; } @@ -73,7 +74,7 @@ export class CaptureManager extends React.Component<{}> { <div className="save" onClick={() => { - LightboxView.SetLightboxDoc(this._document); + LightboxView.Instance.SetLightboxDoc(this._document); this.close(); }}> Save @@ -81,9 +82,9 @@ export class CaptureManager extends React.Component<{}> { <div className="cancel" onClick={() => { - const selected = SelectionManager.Views().slice(); + const selected = SelectionManager.Views.slice(); SelectionManager.DeselectAll(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); + selected.map(dv => dv.props.removeDocument?.(dv.Document)); this.close(); }}> Cancel diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 87ee1b252..07ee777cd 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,5 +1,6 @@ import { observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; +import { OmitKeys, Utils } from "../../Utils"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; @@ -8,27 +9,27 @@ import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; import { ScriptField } from "../../fields/ScriptField"; import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types"; -import { nullAudio, WebField } from "../../fields/URLField"; +import { WebField, nullAudio } from "../../fields/URLField"; import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; -import { OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; -import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; -import { TreeViewType } from "../views/collections/CollectionTreeView"; +import { DocUtils, Docs, DocumentOptions, FInfo } from "../documents/Documents"; import { DashboardView } from "../views/DashboardView"; +import { OverlayView } from "../views/OverlayView"; +import { TreeViewType } from "../views/collections/CollectionTreeView"; import { Colors } from "../views/global/globalEnums"; import { media_state } from "../views/nodes/AudioBox"; -import { DocumentView, OpenWhere } from "../views/nodes/DocumentView"; +import { OpenWhere } from "../views/nodes/DocumentView"; import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox"; import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox"; -import { OverlayView } from "../views/OverlayView"; import { DragManager, dropActionType } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; import { FollowLinkScript } from "./LinkFollower"; import { LinkManager } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; import { ColorScheme } from "./SettingsManager"; +import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; interface Button { @@ -67,7 +68,7 @@ export class CurrentUserUtils { templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, isSystem: true }, template: (opts:DocumentOptions) => Docs.Create.MultirowDocument( [ - Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, isSystem: true }), + Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) ], opts) }, @@ -97,7 +98,7 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, - _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true, + _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, isSystem: true, _forceActive: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; @@ -110,8 +111,8 @@ export class CurrentUserUtils { const tempClicks = DocCast(doc[field]); const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ - { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(documentView?.props.docViewPath().lastElement()?.rootDoc.target).then((target) => target && (target.proto.data = new List([self])))"}, - { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}]; + { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([self])))"}, + { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(this.doubleClickView.${OpenWhere.addRight})`}]; const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; @@ -191,7 +192,7 @@ export class CurrentUserUtils { {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", }); }; const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ - textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 24, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + 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, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); @@ -274,17 +275,17 @@ export class CurrentUserUtils { {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }}, - {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, + {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}}, - {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }}, + {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, dropAction:'move', treeView_Type: TreeViewType.outline, backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true - }, funcs: {title: 'self.text?.Text'}}, + }, funcs: {title: 'this.text?.Text'}}, ]; emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs)); @@ -308,7 +309,7 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script - // { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack>" as any, openFactoryLocation: OpenWhere.overlay}, + // { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack />" as any, openFactoryLocation: OpenWhere.overlay}, ].map(tuple => ( { openFactoryLocation: OpenWhere.addRight, scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', @@ -339,7 +340,7 @@ export class CurrentUserUtils { /// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, toolTip: string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}, hidden?: boolean}[] { - const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())"; + const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(this.target?.data).filter(doc => !docList(this.target.viewed).includes(doc)).length.toString())"; const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails"; return [ { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), ignoreClick: true, icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, @@ -351,7 +352,7 @@ export class CurrentUserUtils { { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}}, { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}}, { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, - ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}})); + ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}})); } /// the empty panel that is filled with whichever left menu button's panel has been selected @@ -467,6 +468,7 @@ export class CurrentUserUtils { const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined); 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.", _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); @@ -479,14 +481,14 @@ export class CurrentUserUtils { const newDashboard = `createNewDashboard()`; const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, - title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true }; + title: "new Dash", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true }; const reqdBtnScript = {onClick: newDashboard,} const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.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 childContextMenuScripts = [`toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)', 'resetDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters + 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 const childContextMenuIcons = ["tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters @@ -558,9 +560,9 @@ export class CurrentUserUtils { const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, color: Colors.BLACK, buttonText: "Empty", icon: "trash", isSystem: true, toolTip: "Empty recently closed",}; - DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); + DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("this.target")}); - if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) { + if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script?.script.originalScript === clearAll("self"))) { recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!]) } return recentlyClosed; @@ -589,7 +591,7 @@ export class CurrentUserUtils { }) static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ - btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true, + btnType: ButtonType.ToolButton, _layout_hideContextMenu: true, _dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]), /*_nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts, }) @@ -628,42 +630,42 @@ export class CurrentUserUtils { } static stackTools(): Button[] { return [ - { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static viewTools(): Button[] { return [ - { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "View All", icon: "object-group", toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "View All", icon: "object-group", toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true, - { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static textTools():Button[] { return [ - { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, + { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, - { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}}, - { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 }, + { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}}, + { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true, subMenu: [ - { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, - { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - ] - }, - { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, - { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, + { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}' }}, + { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + ]}, + { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}}, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, @@ -672,32 +674,37 @@ export class CurrentUserUtils { static inkTools():Button[] { return [ - { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }}, - { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.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(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, - { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, - { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, - { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, - { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnMin: 1}, - { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, + { 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: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, + { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1}, + { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} }, ]; } static schemaTools():Button[] { return [ {title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} }, - {title: "1 Line",toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, - ]; + {title: "1 Line", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, + {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: '{ datavizFromSchema()'} }, ]; } static webTools() { return [ - { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }}, - { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}}, + { title: "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_); }'} }, ]; } + static videoTools() { + return [ + { title: "Snapshot",toolTip: "Take snapshot of current frame", btnType: ButtonType.ClickButton, icon: "camera", scripts: { onClick: '{ return videoSnapshot(); }' }}, + ]; + } static contextMenuTools():Button[] { return [ { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree, @@ -709,17 +716,18 @@ export class CurrentUserUtils { { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, - { title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, - { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected - { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Schema is selected + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected + { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected + { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Schema is selected ]; } @@ -793,11 +801,6 @@ export class CurrentUserUtils { return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } - /// collection of documents rendered in the overlay layer above all tabs and other UI - static setupOverlays(doc: Doc, field = "myOverlayDocs") { - return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", isSystem: true }); - } - static setupPublished(doc:Doc, field = "myPublishedDocs") { return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true }); } @@ -820,7 +823,7 @@ export class CurrentUserUtils { // When the user views one of these documents, it will be added to the sharing documents 'viewed' list field // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents static setupSharedDocs(doc: Doc, sharingDocumentId: string) { - const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(documentView.props.treeViewDoc, 'viewed', documentView.rootDoc);}"; + const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(getSharingDoc(), 'viewed', documentView.Document);}"; const sharedScripts = { treeView_ChildDoubleClick: dblClkScript, } const sharedDocOpts:DocumentOptions = { @@ -832,7 +835,7 @@ export class CurrentUserUtils { // childContextMenuLabels: new List<string>(["Add to Dashboards",]), // childContextMenuIcons: new List<string>(["user-plus",]), "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment, - childDragAction: "embed", isSystem: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, + childDragAction: "embed", isSystem: true, childContentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeView_HideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar _layout_showTitle: "title", treeView_HideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" @@ -901,7 +904,6 @@ export class CurrentUserUtils { 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.setupOverlays(doc); // sets up the overlay panel where documents and other widgets can be added to float over the rest of the dashboard 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); @@ -910,7 +912,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); - DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents + DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards) Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs) @@ -951,12 +953,12 @@ export class CurrentUserUtils { runInAction(() => CurrentUserUtils.ServerVersion = result.version); Doc.CurrentUserEmail = result.email; resolvedPorts = result.resolvedPorts as any; - DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email); if (result.cacheDocumentIds) { const ids = result.cacheDocumentIds.split(";"); const batch = 30000; - for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) { + for (let i = 0; i < ids.length; i += batch) { await DocServer.GetRefFields(ids.slice(i, i+batch)); } } @@ -982,6 +984,7 @@ export class CurrentUserUtils { DashboardView.createNewDashboard(undefined, "guest dashboard"); } else { userDoc.activePage = "home"; + userDoc.noviceMode = true; } } return userDoc; @@ -1022,8 +1025,9 @@ export class CurrentUserUtils { } ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); -ScriptingGlobals.add(function IsExploreMode() { return DocumentView.ExploreMode; }, "is Dash in exploration mode"); +ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode"); ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); +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 0fd7e840c..82c63695c 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -236,7 +236,7 @@ export namespace DictationManager { export const execute = async (phrase: string) => { return UndoManager.RunInBatch(async () => { console.log('PHRASE: ' + phrase); - const targets = SelectionManager.Views(); + const targets = SelectionManager.Views; if (!targets || !targets.length) { return; } @@ -290,7 +290,7 @@ export namespace DictationManager { if (!ctor) { return false; } - return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined; + return Cast(Doc.GetProto(view.Document).data, ctor) !== undefined; }; const validate = (target: DocumentView, types: DocumentType[]) => { @@ -318,7 +318,7 @@ export namespace DictationManager { [ 'clear', { - action: (target: DocumentView) => (Doc.GetProto(target.props.Document).data = new List()), + action: (target: DocumentView) => (Doc.GetProto(target.Document).data = new List()), restrictTo: [DocumentType.COL], }, ], @@ -347,7 +347,7 @@ export namespace DictationManager { action: (target: DocumentView, matches: RegExpExecArray) => { const count = interpretNumber(matches[1]); const what = matches[2]; - const dataDoc = Doc.GetProto(target.props.Document); + const dataDoc = Doc.GetProto(target.Document); const fieldKey = 'data'; if (isNaN(count)) { return; @@ -372,7 +372,7 @@ export namespace DictationManager { expression: /view as (freeform|stacking|masonry|schema|tree)/g, action: (target: DocumentView, matches: RegExpExecArray) => { const mode = matches[1]; - mode && (target.props.Document._type_collection = mode); + mode && (target.Document._type_collection = mode); }, restrictTo: [DocumentType.COL], } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 7cc8afaa6..f730d17fe 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,6 +1,7 @@ -import { action, computed, observable, ObservableSet, observe } from 'mobx'; +import { Howl } from 'howler'; +import { action, computed, makeObservable, observable, ObservableSet, observe } from 'mobx'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { AclAdmin, AclEdit, Animation } from '../../fields/DocSymbols'; +import { AclAdmin, AclEdit, Animation, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; @@ -10,21 +11,27 @@ import { CollectionViewType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { TabDocView } from '../views/collections/TabDocView'; import { LightboxView } from '../views/LightboxView'; -import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; +import { FocusViewOptions } from '../views/nodes/FieldView'; import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { PresBox } from '../views/nodes/trails'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; -const { Howl } = require('howler'); export class DocumentManager { + private static _instance: DocumentManager; + public static get Instance(): DocumentManager { + return this._instance || (this._instance = new this()); + } + //global holds all of the nodes (regardless of which collection they're in) @observable _documentViews = new Set<DocumentView>(); - @observable public LinkAnchorBoxViews: DocumentView[] = []; - @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; + @observable.shallow public CurrentlyLoading: Doc[] = []; + @observable.shallow public LinkAnchorBoxViews: DocumentView[] = []; + @observable.shallow public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; @computed public get DocumentViews() { - return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(view.docViewPath))); + return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.Contains(view))); } public AddDocumentView(dv: DocumentView) { this._documentViews.add(dv); @@ -33,24 +40,19 @@ export class DocumentManager { this._documentViews.delete(dv); } - private static _instance: DocumentManager; - public static get Instance(): DocumentManager { - return this._instance || (this._instance = new this()); - } - //private constructor so no other class can create a nodemanager private constructor() { - if (!Doc.CurrentlyLoading) Doc.CurrentlyLoading = []; - observe(Doc.CurrentlyLoading, change => { + makeObservable(this); + observe(this.CurrentlyLoading, change => { // watch CurrentlyLoading-- when something is loaded, it's removed from the list and we have to update its icon if it were iconified since LoadingBox icons are different than the media they become switch (change.type as any) { case 'update': break; case 'remove': - // DocumentManager.Instance.getAllDocumentViews(change as any).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); + // DocumentManager.Instance.getAllDocumentViews(change as any).forEach(dv => StrCast(dv.Document.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); break; case 'splice': - (change as any).removed.forEach((doc: Doc) => DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()))); + (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; } }); @@ -71,7 +73,7 @@ export class DocumentManager { return false; }; callAddViewFuncs = (view: DocumentView) => { - const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc); + const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.Document); if (callFuncs.length) { this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc)); const intTimer = setInterval( @@ -88,11 +90,11 @@ export class DocumentManager { @action public AddView = (view: DocumentView) => { - if (view.props.LayoutTemplateString?.includes(KeyValueBox.name)) return; - if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1'; - const link = view.rootDoc; - this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => + if (view._props.LayoutTemplateString?.includes(KeyValueBox.name)) return; + if (view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + const viewAnchorIndex = view._props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1'; + const link = view.Document; + this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.Document, link) && !dv._props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => this.LinkedDocumentViews.push({ a: viewAnchorIndex === 'link_anchor_2' ? otherView : view, b: viewAnchorIndex === 'link_anchor_2' ? view : otherView, @@ -115,7 +117,7 @@ export class DocumentManager { }) ); - if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + if (view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { @@ -128,13 +130,13 @@ export class DocumentManager { public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; DocumentManager.Instance.DocumentViews.forEach(view => { - if (view.rootDoc[Id] === id) { + if (view.Document[Id] === id) { toReturn.push(view); } }); if (toReturn.length === 0) { DocumentManager.Instance.DocumentViews.forEach(view => { - if (Doc.GetProto(view.rootDoc)?.[Id] === id) { + if (view.Document[DocData]?.[Id] === id) { toReturn.push(view); } }); @@ -152,42 +154,42 @@ export class DocumentManager { return passes.reduce( (toReturn, pass) => toReturn ?? - docViewArray.filter(view => view.rootDoc === target).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ?? - docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, target)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection), + docViewArray.filter(view => view.Document === target).find(view => !pass || view.containerViewPath?.().lastElement() === preferredCollection) ?? + docViewArray.filter(view => Doc.AreProtosEqual(view.Document, target)).find(view => !pass || view.containerViewPath?.().lastElement() === preferredCollection), undefined as Opt<DocumentView> ); } public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const views: DocumentView[] = []; - DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); - return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined); + 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); }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc); - const views = this.getDocumentViews(toFind); //.filter(view => view.rootDoc !== originatingDoc); - return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined); + 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.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) || - // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href) - // )?.rootDoc ?? + // ((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; const toReturn: DocumentView[] = []; - const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); - const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); + const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.Contains(view)); + const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.Contains(view)); // heuristic to return the "best" documents first: // choose a document in the lightbox first // choose an exact match over an embedding match - lightViews.map(view => view.rootDoc === toFind && toReturn.push(view)); - lightViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view)); - docViews.map(view => view.rootDoc === toFind && toReturn.push(view)); - docViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view)); + lightViews.map(view => view.Document === toFind && toReturn.push(view)); + lightViews.map(view => view.Document !== toFind && Doc.AreProtosEqual(view.Document, toFind) && toReturn.push(view)); + docViews.map(view => view.Document === toFind && toReturn.push(view)); + docViews.map(view => view.Document !== toFind && Doc.AreProtosEqual(view.Document, toFind) && toReturn.push(view)); return toReturn; } @@ -223,7 +225,7 @@ export class DocumentManager { } public static removeOverlayViews() { - DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined))); + DocumentManager._overlayViews?.forEach(view => view.setTextHtmlOverlay(undefined, undefined)); DocumentManager._overlayViews?.clear(); } static _overlayViews = new ObservableSet<DocumentView>(); @@ -235,12 +237,12 @@ export class DocumentManager { // shows a documentView by: // traverses down through the viewPath of contexts to the view: // focusing on each context - public showDocumentView = async (targetDocView: DocumentView, options: DocFocusOptions) => { - const docViewPath = targetDocView.docViewPath.slice(); + public showDocumentView = async (targetDocView: DocumentView, options: FocusViewOptions) => { + const docViewPath = [...(targetDocView.containerViewPath?.() ?? []), targetDocView]; let rootContextView = docViewPath.shift(); await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined, focused: false }))); - if (options.toggleTarget && (!options.didMove || targetDocView.rootDoc.hidden)) targetDocView.rootDoc.hidden = !targetDocView.rootDoc.hidden; - else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.rootDoc, options.openLocation); + 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); }; // shows a document by first: @@ -251,7 +253,7 @@ 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: DocFocusOptions, // options for how to navigate to target + options: 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. ) => { Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); @@ -273,14 +275,14 @@ export class DocumentManager { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc); const contextView = this.getDocumentView(DocCast(target.embedContainer)); - if (contextView?.docView?._componentView?.addDocTab?.(target, OpenWhere.lightbox)) { + if (contextView?.ComponentView?.addDocTab?.(target, OpenWhere.lightbox)) { await new Promise<void>(waitres => setTimeout(() => waitres())); } } docContextPath.shift(); const childViewIterator = async (docView: DocumentView) => { const innerDoc = docContextPath.shift(); - return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined }; + return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc, options)) ?? this.getDocumentView(innerDoc) : undefined }; }; if (rootContextView) { @@ -292,58 +294,56 @@ export class DocumentManager { focusViewsInPath = async ( docView: DocumentView, // - options: DocFocusOptions, + options: FocusViewOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean }> ) => { let contextView: DocumentView | undefined; // view containing context that contains target let focused = false; while (true) { - if (docView.rootDoc.layout_fieldKey === 'layout_icon') { + if (docView.Document.layout_fieldKey === 'layout_icon') { await new Promise<void>(res => docView.iconify(res)); options.didMove = true; } - const nextFocus = docView.props.focus(docView.rootDoc, options); // focus the view within its container + 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 const { childDocView, viewSpec } = await iterator(docView); - if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView, focused }; - contextView = docView; + if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.Document, docView, contextView, focused }; + contextView = options.anchorDoc?.layout_unrendered && !childDocView.Document.layout_unrendered ? childDocView : docView; docView = childDocView; } }; @action - restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: DocFocusOptions, contextView: Opt<DocumentView>, targetDoc: Doc) { + restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: FocusViewOptions, contextView: Opt<DocumentView>, targetDoc: Doc) { if (viewSpec && docView) { //if (docView.ComponentView instanceof FormattedTextBox) - //viewSpec !== docView.rootDoc && + //viewSpec !== docView.Document && docView.ComponentView?.focus?.(viewSpec, options); PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500); - Doc.linkFollowHighlight(viewSpec ? [docView.rootDoc, viewSpec] : docView.rootDoc, undefined, options.effect); - if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.rootDoc._layout_currentTimecode)); - if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc); - if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden; - if (options.effect) docView.rootDoc[Animation] = options.effect; + Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect); + if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.Document._layout_currentTimecode)); + if (options.playAudio) DocumentManager.playAudioAnno(docView.Document); + 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 && viewSpec.text_html) { + if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && targetDoc.text_html) { // if the docView is a text anchor, the contextView is the PDF/Web/Text doc - contextView.htmlOverlayEffect = StrCast(options?.effect?.presentation_effect, StrCast(options?.effect?.followLinkAnimEffect)); - contextView.textHtmlOverlay = StrCast(targetDoc.text_html); + contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect); DocumentManager._overlayViews.add(contextView); } Doc.AddUnHighlightWatcher(() => { - docView.rootDoc[Animation] = undefined; + docView.Document[Animation] = undefined; DocumentManager.removeOverlayViews(); - contextView && (contextView.htmlOverlayEffect = ''); }); } } } -export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { +export function DocFocusOrOpen(doc: Doc, options: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { const func = () => { const cv = DocumentManager.Instance.getDocumentView(containingDoc); const dv = DocumentManager.Instance.getDocumentView(doc, cv); - if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) { - DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc)); + if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { + DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); } else { const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; @@ -351,7 +351,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => { const cv = DocumentManager.Instance.getDocumentView(containingDoc); const dv = DocumentManager.Instance.getDocumentView(doc, cv); - dv && Doc.linkFollowHighlight(dv.rootDoc); + dv && Doc.linkFollowHighlight(dv.Document); }); } }; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index ea13eaa5b..a6ad0f1b3 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -4,15 +4,17 @@ import { Doc, Field, Opt, StrListCast } from '../../fields/Doc'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { ScriptField } from '../../fields/ScriptField'; -import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; +import { ScriptCast, StrCast } from '../../fields/Types'; import { emptyFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; -import * as globalCssVariables from '../views/global/globalCssVariables.scss'; +import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../views/nodes/DocumentView'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; +import { DocData } from '../../fields/DocSymbols'; +const { default : { contextMenuZindex } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore export type dropActionType = 'embed' | 'copy' | 'move' | 'add' | 'same' | 'inSame' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove @@ -57,7 +59,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () export namespace DragManager { let dragDiv: HTMLDivElement; let dragLabel: HTMLDivElement; - export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void>; + export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => boolean>; export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; export function Root() { @@ -84,7 +86,16 @@ export namespace DragManager { // event called when the drag operation results in a drop action export class DropEvent { - constructor(readonly x: number, readonly y: number, readonly complete: DragCompleteEvent, readonly shiftKey: boolean, readonly altKey: boolean, readonly metaKey: boolean, readonly ctrlKey: boolean, readonly embedKey: boolean) {} + constructor( + readonly x: number, + readonly y: number, + readonly complete: DragCompleteEvent, + readonly shiftKey: boolean, + readonly altKey: boolean, + readonly metaKey: boolean, + readonly ctrlKey: boolean, + readonly embedKey: boolean + ) {} } // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated @@ -132,7 +143,7 @@ export namespace DragManager { this.linkSourceGetAnchor = linkSourceGetAnchor; } get dragDocument() { - return this.linkDragView.props.Document; + return this.linkDragView.Document; } linkSourceGetAnchor: () => Doc; linkSourceDoc?: Doc; @@ -170,7 +181,7 @@ export namespace DragManager { } }; - export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer { + export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer { if ('canDrop' in element.dataset) { throw new Error("Element is already droppable, can't make it droppable again"); } @@ -178,13 +189,13 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.dropAction) as dropActionType); + (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc.dropAction) as dropActionType); }; element.addEventListener('dashOnDrop', handler); - doc && element.addEventListener('dashPreDrop', preDropHandler); + element.addEventListener('dashPreDrop', preDropHandler); return () => { element.removeEventListener('dashOnDrop', handler); - doc && element.removeEventListener('dashPreDrop', preDropHandler); + element.removeEventListener('dashPreDrop', preDropHandler); delete element.dataset.canDrop; }; } @@ -198,7 +209,7 @@ export namespace DragManager { }; const finishDrag = async (e: DragCompleteEvent) => { const docDragData = e.docDragData; - setTimeout(() => dragData.draggedViews.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragEnding())); + 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) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; @@ -208,16 +219,14 @@ export namespace DragManager { !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : docDragData.dropAction === 'embed' - ? Doc.BestEmbedding(d) - : docDragData.dropAction === 'add' - ? d - : docDragData.dropAction === 'proto' - ? Doc.GetProto(d) - : docDragData.dropAction === 'copy' - ? ( - await Doc.MakeClone(d) - ).clone - : d + ? Doc.BestEmbedding(d) + : docDragData.dropAction === 'add' + ? d + : docDragData.dropAction === 'proto' + ? d[DocData] + : docDragData.dropAction === 'copy' + ? (await Doc.MakeClone(d)).clone + : d ) ) ).filter(d => d); @@ -234,7 +243,7 @@ export namespace DragManager { }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); - dragData.draggedViews.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragStarting()); + dragData.draggedViews.forEach(view => view.props.dragStarting?.()); return true; } @@ -242,9 +251,9 @@ export namespace DragManager { 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) { 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 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields + 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 initialize?.(bd); - Doc.GetProto(bd)['onClick-paramFieldKeys'] = new List<string>(params); + bd[DocData]['onClick-paramFieldKeys'] = new List<string>(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); return e; }; @@ -260,7 +269,7 @@ export namespace DragManager { // drags a linker button and creates a link on drop export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | ((addAsAnnotation: boolean) => Doc), downX: number, downY: number, options?: DragOptions) { - StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.rootDoc), downX, downY, options); + StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.Document), downX, downY, options); } // drags a column from a schema view @@ -286,14 +295,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, i) => { 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, i) => { 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; @@ -317,19 +326,19 @@ export namespace DragManager { return drag; }; return { - x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), - y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()), + x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.VertSnapLines), + y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.HorizSnapLines), }; } - export let docsBeingDragged: Doc[] = observable([] as Doc[]); + 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) { - if (dragData.dropAction === 'none' || DocumentView.ExploreMode) return; + if (dragData.dropAction === 'none' || SnappingManager.ExploreMode) return; DocDragData = dragData as DocumentDragData; const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); eles = eles.filter(e => e); - CanEmbed = dragData.canEmbed || false; + SnappingManager.SetCanEmbed(dragData.canEmbed || false); if (!dragDiv) { dragDiv = document.createElement('div'); dragDiv.className = 'dragManager-dragDiv'; @@ -360,11 +369,18 @@ export namespace DragManager { const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element - let useDim = false; // if doc is rotated by freeformview, then the dragged elements width and height won't reflect the unrotated dimensions, so we need to rely on the element knowing its own width/height. \ + // bcz: used to be false, but that made dragging collection w/ native dim's not work... + let useDim = true; // if doc is rotated by freeformview, then the dragged elements width and height won't reflect the unrotated dimensions, so we need to rely on the element knowing its own width/height. \ // if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu) - if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') { - ele = ele.parentElement.parentElement.parentElement; - rot.push(Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0)); + let rotation: number | undefined; + for (let parEle: HTMLElement | null | undefined = ele.parentElement; parEle; parEle = parEle?.parentElement) { + if (parEle.className === CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName) { + rotation = (rotation ?? 0) + Number(parEle.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0); + } + parEle = parEle.parentElement; + } + if (rotation !== undefined) { + rot.push(rotation); } else { useDim = true; rot.push(0); @@ -420,7 +436,7 @@ export namespace DragManager { color: 'black', transition: 'none', borderRadius: getComputedStyle(ele).borderRadius, - zIndex: globalCssVariables.contextMenuZindex, + zIndex: contextMenuZindex, transformOrigin: '0 0', width, height, @@ -455,7 +471,7 @@ export namespace DragManager { runInAction(() => docsBeingDragged.push(...docsToDrag)); const hideDragShowOriginalElements = (hide: boolean) => { - dragLabel.style.display = hide && !CanEmbed ? '' : 'none'; + dragLabel.style.display = hide && !SnappingManager.CanEmbed ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; @@ -475,13 +491,14 @@ export namespace DragManager { }; const cleanupDrag = action((undo: boolean) => { - (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragEnding()); + (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; const moveHandler = (e: PointerEvent) => { @@ -489,7 +506,7 @@ export namespace DragManager { if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'embed' : dragData.defaultDropAction; } - if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header' || e?.shiftKey) && dragData.draggedDocuments.length === 1) { + if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header') && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; @@ -555,7 +572,7 @@ export namespace DragManager { ); scrollAwaiter && clearTimeout(scrollAwaiter); - SnappingManager.GetIsDragging() && (scrollAwaiter = setTimeout(autoScrollHandler, 25)); + SnappingManager.IsDragging && (scrollAwaiter = setTimeout(autoScrollHandler, 25)); }; scrollAwaiter && clearTimeout(scrollAwaiter); scrollAwaiter = setTimeout(autoScrollHandler, 250); @@ -588,7 +605,7 @@ export namespace DragManager { altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, - embedKey: CanEmbed, + embedKey: SnappingManager.CanEmbed, }, }; target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); @@ -602,7 +619,7 @@ export namespace DragManager { ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) { if (readOnly) { - return SelectionManager.Views().some(dv => dv.rootDoc.keepZWhenDragged); + return SelectionManager.Views.some(dv => dv.Document.keepZWhenDragged); } - SelectionManager.Views().map(dv => (dv.rootDoc.keepZWhenDragged = !dv.rootDoc.keepZWhenDragged)); + SelectionManager.Views.map(dv => (dv.Document.keepZWhenDragged = !dv.Document.keepZWhenDragged)); }); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 2c371f28e..f62ec8f83 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,4 +1,5 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; import { listSpec } from '../../fields/Schema'; @@ -12,7 +13,7 @@ import { DragManager } from './DragManager'; import { ScriptingGlobals } from './ScriptingGlobals'; export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { - if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated + if (templateField) doc[DocData].title = templateField; /// the title determines which field is being templated doc.isTemplateDoc = makeTemplate(doc, first, rename); return doc; } @@ -33,7 +34,7 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und let any = false; docs.forEach(d => { if (!StrCast(d.title).startsWith('-')) { - any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any; + any = Doc.MakeMetadataFieldTemplate(d, layoutDoc[DocData]) || any; } else if (d.type === DocumentType.COL || d.data instanceof RichTextField) { any = makeTemplate(d, false) || any; } @@ -41,12 +42,12 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und if (first) { if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template - any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any; + any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData]) || any; } } if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { if (!StrCast(layoutDoc.title).startsWith('-')) { - any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc)); + any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData]); } } rename && (doc.title = rename); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 8973306bf..f4f879208 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; @@ -17,6 +17,7 @@ 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 @@ -27,12 +28,12 @@ export interface UserOptions { } @observer -export class GroupManager extends React.Component<{}> { +export class GroupManager extends ObservableReactComponent<{}> { 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. @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. - @observable currentGroup: Opt<Doc>; // the currently selected group. + @observable currentGroup: Opt<Doc> = undefined; // the currently selected group. @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button @@ -41,6 +42,7 @@ export class GroupManager extends React.Component<{}> { constructor(props: Readonly<{}>) { super(props); + makeObservable(this); GroupManager.Instance = this; } @@ -52,7 +54,7 @@ export class GroupManager extends React.Component<{}> { * Fetches the list of users stored on the database. */ populateUsers = async () => { - if (Doc.UserDoc()[Id] !== '__guest__') { + if (Doc.UserDoc()[Id] !== Utils.GuestID()) { const userList = await RequestPromise.get(Utils.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))); @@ -258,10 +260,7 @@ export class GroupManager extends React.Component<{}> { 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/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 7de0f336f..894583711 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -19,35 +19,38 @@ interface GroupMemberViewProps { @observer export class GroupMemberView extends React.Component<GroupMemberViewProps> { @observable private memberSort: 'ascending' | 'descending' | 'none' = 'none'; + get group() { + return this.props.group; + } private get editingInterface() { - let members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : []; + let members: string[] = this.group ? JSON.parse(StrCast(this.group.members)) : []; members = this.memberSort === 'ascending' ? members.sort() : this.memberSort === 'descending' ? members.sort().reverse() : members; - const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : []; + const options: UserOptions[] = this.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.group.members)) as string[]).includes(option.value)) : []; - const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group); + const hasEditAccess = GroupManager.Instance.hasEditAccess(this.group); - return !this.props.group ? null : ( + return !this.group ? null : ( <div className="editing-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className="editing-header"> <input className="group-title" style={{ marginLeft: !hasEditAccess ? '-14%' : 0 }} - value={StrCast(this.props.group.title || this.props.group.groupName)} - onChange={e => (this.props.group.title = e.currentTarget.value)} + value={StrCast(this.group.title || this.group.groupName)} + onChange={e => (this.group.title = e.currentTarget.value)} disabled={!hasEditAccess}></input> <div className={'memberView-closeButton'}> <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={action(this.props.onCloseButtonClick)} color={StrCast(Doc.UserDoc().userColor)} /> </div> - {GroupManager.Instance.hasEditAccess(this.props.group) ? ( + {GroupManager.Instance.hasEditAccess(this.group) ? ( <div className="group-buttons"> <div style={{ border: StrCast(Doc.UserDoc().userColor) }}> <Select className="add-member-dropdown" isSearchable={true} options={options} - onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.props.group, (selectedOption as UserOptions).value)} + onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.group, (selectedOption as UserOptions).value)} placeholder={'Add members'} value={null} styles={{ @@ -72,8 +75,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> { }} /> </div> - <div className={'delete-button'}> - <Button text={'Delete Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.deleteGroup(this.props.group)} /> + <div className="delete-button"> + <Button text="Delete Group" type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.deleteGroup(this.group)} /> </div> </div> ) : null} @@ -81,14 +84,14 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> { Emails {this.memberSort === 'ascending' ? '↑' : this.memberSort === 'descending' ? '↓' : ''} {/* → */} </div> </div> - <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} /> + <div className="style-divider" style={{ background: StrCast(Doc.UserDoc().userColor) }} /> <div className="editing-contents" style={{ height: hasEditAccess ? '62%' : '85%' }}> {members.map(member => ( <div className="editing-row" key={member}> <div className="user-email">{member}</div> {hasEditAccess ? ( - <div className={'remove-button'} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> - <IconButton icon={<FontAwesomeIcon icon={'trash-alt'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)} /> + <div className={'remove-button'} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)}> + <IconButton icon={<FontAwesomeIcon icon={'trash-alt'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.group, member)} /> </div> ) : null} </div> diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 18aee6444..2f1a336cc 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -85,7 +85,7 @@ export namespace HistoryUtil { }; function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) { - function parse(parser: ParserValue, value: string | string[] | null | undefined) { + function parse(parser: ParserValue, value: string | (string | null)[] | null | undefined) { if (value === undefined || value === null) { return value; } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 151f18d6f..c5f307f44 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -1,16 +1,15 @@ -import { StrCast, Cast } from '../../fields/Types'; -import { SearchUtil } from './SearchUtil'; import { action, runInAction } from 'mobx'; +import { simulateMouseClick } from '../../Utils'; import { Doc, Opt } from '../../fields/Doc'; +import { Cast, StrCast } from '../../fields/Types'; +import { WebField } from '../../fields/URLField'; import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; -import { SelectionManager } from './SelectionManager'; -import { WebField } from '../../fields/URLField'; -import { DocumentManager } from './DocumentManager'; import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; -import { simulateMouseClick, Utils } from '../../Utils'; import { DocumentView } from '../views/nodes/DocumentView'; -import { Id } from '../../fields/FieldSymbols'; +import { DocumentManager } from './DocumentManager'; +import { SearchUtil } from './SearchUtil'; +import { SelectionManager } from './SelectionManager'; export namespace Hypothesis { /** @@ -27,19 +26,19 @@ export namespace Hypothesis { * Search for a WebDocument whose url field matches the given uri, return undefined if not found */ export const findWebDoc = async (uri: string) => { - const currentDoc = SelectionManager.Docs().lastElement(); + const currentDoc = SelectionManager.Docs.lastElement(); if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise const results: Doc[] = []; - await SearchUtil.Search('web', true).then( - action(async (res: SearchUtil.DocSearchResult) => { - const docs = res.docs; - const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); - filteredDocs.forEach(doc => { - uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? - }); - }) - ); + // await SearchUtil.Search('web', true).then( + // action(async (res: SearchUtil.DocSearchResult) => { + // const docs = res.docs; + // const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); + // filteredDocs.forEach(doc => { + // uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? + // }); + // }) + // ); const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc)); return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen @@ -181,7 +180,7 @@ export namespace Hypothesis { }) ); const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target); - const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const position = targetView?.screenToViewTransform().inverse().transformPoint(0, 0); targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false); }, 300); diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 1a4c2450e..398ba3c04 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,440 +1,439 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { BatchedArray } from 'array-batcher'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import { extname } from 'path'; -import Measure, { ContentRect } from 'react-measure'; -import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; -import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { BoolCast, Cast, NumCast } from '../../../fields/Types'; -import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes'; -import { Utils } from '../../../Utils'; -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { Networking } from '../../Network'; -import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; -import { DocumentManager } from '../DocumentManager'; -import './DirectoryImportBox.scss'; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry'; -import React = require('react'); -import _ = require('lodash'); +// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +// import { BatchedArray } from 'array-batcher'; +// import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +// import { observer } from 'mobx-react'; +// import { extname } from 'path'; +// import Measure, { ContentRect } from 'react-measure'; +// import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; +// import { Id } from '../../../fields/FieldSymbols'; +// import { List } from '../../../fields/List'; +// import { listSpec } from '../../../fields/Schema'; +// import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +// import { BoolCast, Cast, NumCast } from '../../../fields/Types'; +// import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes'; +// import { Utils } from '../../../Utils'; +// import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +// import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +// import { DocumentType } from '../../documents/DocumentTypes'; +// import { Networking } from '../../Network'; +// import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; +// import { DocumentManager } from '../DocumentManager'; +// import './DirectoryImportBox.scss'; +// import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry'; +// import * as React from 'react'; -const unsupported = ['text/html', 'text/plain']; +// const unsupported = ['text/html', 'text/plain']; -@observer -export class DirectoryImportBox extends React.Component<FieldViewProps> { - private selector = React.createRef<HTMLInputElement>(); - @observable private top = 0; - @observable private left = 0; - private dimensions = 50; - @observable private phase = ''; - private disposer: Opt<IReactionDisposer>; +// @observer +// export class DirectoryImportBox extends React.Component<FieldViewProps> { +// private selector = React.createRef<HTMLInputElement>(); +// @observable private top = 0; +// @observable private left = 0; +// private dimensions = 50; +// @observable private phase = ''; +// private disposer: Opt<IReactionDisposer>; - @observable private entries: ImportMetadataEntry[] = []; +// @observable private entries: ImportMetadataEntry[] = []; - @observable private quota = 1; - @observable private completed = 0; +// @observable private quota = 1; +// @observable private completed = 0; - @observable private uploading = false; - @observable private removeHover = false; +// @observable private uploading = false; +// @observable private removeHover = false; - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DirectoryImportBox, fieldKey); - } +// public static LayoutString(fieldKey: string) { +// return FieldView.LayoutString(DirectoryImportBox, fieldKey); +// } - constructor(props: FieldViewProps) { - super(props); - const doc = this.props.Document; - this.editingMetadata = this.editingMetadata || false; - this.persistent = this.persistent || false; - !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>()); - } +// constructor(props: FieldViewProps) { +// super(props); +// const doc = this.props.Document; +// this.editingMetadata = this.editingMetadata || false; +// this.persistent = this.persistent || false; +// !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>()); +// } - @computed - private get editingMetadata() { - return BoolCast(this.props.Document.editingMetadata); - } +// @computed +// private get editingMetadata() { +// return BoolCast(this.props.Document.editingMetadata); +// } - private set editingMetadata(value: boolean) { - this.props.Document.editingMetadata = value; - } +// private set editingMetadata(value: boolean) { +// this.props.Document.editingMetadata = value; +// } - @computed - private get persistent() { - return BoolCast(this.props.Document.persistent); - } +// @computed +// private get persistent() { +// return BoolCast(this.props.Document.persistent); +// } - private set persistent(value: boolean) { - this.props.Document.persistent = value; - } +// private set persistent(value: boolean) { +// this.props.Document.persistent = value; +// } - handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { - runInAction(() => { - this.uploading = true; - this.phase = 'Initializing download...'; - }); +// handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { +// runInAction(() => { +// this.uploading = true; +// this.phase = 'Initializing download...'; +// }); - const docs: Doc[] = []; +// const docs: Doc[] = []; - const files = e.target.files; - if (!files || files.length === 0) return; +// const files = e.target.files; +// if (!files || files.length === 0) return; - const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0]; +// const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0]; - const validated: File[] = []; - for (let i = 0; i < files.length; i++) { - const file = files.item(i); - if (file && !unsupported.includes(file.type)) { - const ext = extname(file.name).toLowerCase(); - if (AcceptableMedia.imageFormats.includes(ext)) { - validated.push(file); - } - } - } +// const validated: File[] = []; +// for (let i = 0; i < files.length; i++) { +// const file = files.item(i); +// if (file && !unsupported.includes(file.type)) { +// const ext = extname(file.name).toLowerCase(); +// if (AcceptableMedia.imageFormats.includes(ext)) { +// validated.push(file); +// } +// } +// } - runInAction(() => { - this.quota = validated.length; - this.completed = 0; - }); +// runInAction(() => { +// this.quota = validated.length; +// this.completed = 0; +// }); - const sizes: number[] = []; - const modifiedDates: number[] = []; +// const sizes: number[] = []; +// const modifiedDates: number[] = []; - runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`)); +// runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`)); - const batched = BatchedArray.from(validated, { batchSize: 15 }); - const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => { - batch.forEach(file => { - sizes.push(file.size); - modifiedDates.push(file.lastModified); - }); - collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file =>({file}))))); - runInAction(() => (this.completed += batch.length)); - }); +// const batched = BatchedArray.from(validated, { batchSize: 15 }); +// const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => { +// batch.forEach(file => { +// sizes.push(file.size); +// modifiedDates.push(file.lastModified); +// }); +// collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file => ({ file }))))); +// runInAction(() => (this.completed += batch.length)); +// }); - await Promise.all( - uploads.map(async response => { - const { - source: { type }, - result, - } = response; - if (result instanceof Error) { - return; - } - const { accessPaths, exifData } = result; - const path = Utils.prepend(accessPaths.agnostic.client); - const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 })); - const { data, error } = exifData; - if (document) { - Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); - docs.push(document); - } - }) - ); +// await Promise.all( +// uploads.map(async response => { +// const { +// source: { mimetype }, +// result, +// } = response; +// if (result instanceof Error) { +// return; +// } +// const { accessPaths, exifData } = result; +// const path = Utils.prepend(accessPaths.agnostic.client); +// const document = mimetype && (await DocUtils.DocumentFromType(mimetype, path, { _width: 300 })); +// const { data, error } = exifData; +// if (document) { +// Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); +// docs.push(document); +// } +// }) +// ); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - doc.size = sizes[i]; - doc.modified = modifiedDates[i]; - this.entries.forEach(entry => { - const target = entry.onDataDoc ? Doc.GetProto(doc) : doc; - target[entry.key] = entry.value; - }); - } +// for (let i = 0; i < docs.length; i++) { +// const doc = docs[i]; +// doc.size = sizes[i]; +// doc.modified = modifiedDates[i]; +// this.entries.forEach(entry => { +// const target = entry.onDataDoc ? Doc.GetProto(doc) : doc; +// target[entry.key] = entry.value; +// }); +// } - const doc = this.props.Document; - const height: number = NumCast(doc.height) || 0; - const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0; - const options: DocumentOptions = { - title: `Import of ${directory}`, - _width: 1105, - _height: 500, - _chromeHidden: true, - x: NumCast(doc.x), - y: NumCast(doc.y) + offset, - }; - const parent = this.props.DocumentView?.().props.docViewPath().lastElement(); - if (parent?.rootDoc.type === DocumentType.COL) { - let importContainer: Doc; - if (docs.length < 50) { - importContainer = Docs.Create.MasonryDocument(docs, options); - } else { - const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')]; - importContainer = Docs.Create.SchemaDocument(headers, docs, options); - } - runInAction(() => (this.phase = 'External: uploading files to Google Photos...')); - await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); - Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer); - !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); - DocumentManager.Instance.showDocument(importContainer, { willZoomCentered: true }); - } +// const doc = this.props.Document; +// const height: number = NumCast(doc.height) || 0; +// const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0; +// const options: DocumentOptions = { +// title: `Import of ${directory}`, +// _width: 1105, +// _height: 500, +// _chromeHidden: true, +// x: NumCast(doc.x), +// y: NumCast(doc.y) + offset, +// }; +// const parent = this.props.DocumentView?.().containerViewPath().lastElement(); +// if (parent?.Document.type === DocumentType.COL) { +// let importContainer: Doc; +// if (docs.length < 50) { +// importContainer = Docs.Create.MasonryDocument(docs, options); +// } else { +// const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')]; +// importContainer = Docs.Create.SchemaDocument(headers, docs, options); +// } +// runInAction(() => (this.phase = 'External: uploading files to Google Photos...')); +// await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); +// Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer); +// !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); +// DocumentManager.Instance.showDocument(importContainer, { willZoomCentered: true }); +// } - runInAction(() => { - this.uploading = false; - this.quota = 1; - this.completed = 0; - }); - }; +// runInAction(() => { +// this.uploading = false; +// this.quota = 1; +// this.completed = 0; +// }); +// }; - componentDidMount() { - this.selector.current!.setAttribute('directory', ''); - this.selector.current!.setAttribute('webkitdirectory', ''); - this.disposer = reaction( - () => this.completed, - completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)) - ); - } +// componentDidMount() { +// this.selector.current!.setAttribute('directory', ''); +// this.selector.current!.setAttribute('webkitdirectory', ''); +// this.disposer = reaction( +// () => this.completed, +// completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)) +// ); +// } - componentWillUnmount() { - this.disposer && this.disposer(); - } +// componentWillUnmount() { +// this.disposer && this.disposer(); +// } - @action - preserveCentering = (rect: ContentRect) => { - const bounds = rect.offset!; - if (bounds.width === 0 || bounds.height === 0) { - return; - } - const offset = this.dimensions / 2; - this.left = bounds.width / 2 - offset; - this.top = bounds.height / 2 - offset; - }; +// @action +// preserveCentering = (rect: ContentRect) => { +// const bounds = rect.offset!; +// if (bounds.width === 0 || bounds.height === 0) { +// return; +// } +// const offset = this.dimensions / 2; +// this.left = bounds.width / 2 - offset; +// this.top = bounds.height / 2 - offset; +// }; - @action - addMetadataEntry = async () => { - const entryDoc = new Doc(); - entryDoc.checked = false; - entryDoc.key = keyPlaceholder; - entryDoc.value = valuePlaceholder; - Doc.AddDocToList(this.props.Document, 'data', entryDoc); - }; +// @action +// addMetadataEntry = async () => { +// const entryDoc = new Doc(); +// entryDoc.checked = false; +// entryDoc.key = keyPlaceholder; +// entryDoc.value = valuePlaceholder; +// Doc.AddDocToList(this.props.Document, 'data', entryDoc); +// }; - @action - remove = async (entry: ImportMetadataEntry) => { - const metadata = await DocListCastAsync(this.props.Document.data); - if (metadata) { - let index = this.entries.indexOf(entry); - if (index !== -1) { - runInAction(() => this.entries.splice(index, 1)); - index = metadata.indexOf(entry.props.Document); - if (index !== -1) { - metadata.splice(index, 1); - } - } - } - }; +// @action +// remove = async (entry: ImportMetadataEntry) => { +// const metadata = await DocListCastAsync(this.props.Document.data); +// if (metadata) { +// let index = this.entries.indexOf(entry); +// if (index !== -1) { +// runInAction(() => this.entries.splice(index, 1)); +// index = metadata.indexOf(entry.props.Document); +// if (index !== -1) { +// metadata.splice(index, 1); +// } +// } +// } +// }; - render() { - const dimensions = 50; - const entries = DocListCast(this.props.Document.data); - const isEditing = this.editingMetadata; - const completed = this.completed; - const quota = this.quota; - const uploading = this.uploading; - const showRemoveLabel = this.removeHover; - const persistent = this.persistent; - let percent = `${(completed / quota) * 100}`; - percent = percent.split('.')[0]; - percent = percent.startsWith('100') ? '99' : percent; - const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; - const message = <span className={'phase'}>{this.phase}</span>; - const centerPiece = this.phase.includes('Google Photos') ? ( - <img - src={'/assets/google_photos.png'} - style={{ - transition: '0.4s opacity ease', - width: 30, - height: 30, - opacity: uploading ? 1 : 0, - pointerEvents: 'none', - position: 'absolute', - left: 12, - top: this.top + 10, - fontSize: 18, - color: 'white', - marginLeft: this.left + marginOffset, - }} - /> - ) : ( - <div - style={{ - transition: '0.4s opacity ease', - opacity: uploading ? 1 : 0, - pointerEvents: 'none', - position: 'absolute', - left: 10, - top: this.top + 12.3, - fontSize: 18, - color: 'white', - marginLeft: this.left + marginOffset, - }}> - {percent}% - </div> - ); - return ( - <Measure offset onResize={this.preserveCentering}> - {({ measureRef }) => ( - <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}> - {message} - <input - id={'selector'} - ref={this.selector} - onChange={this.handleSelection} - type="file" - style={{ - position: 'absolute', - display: 'none', - }} - /> - <label - htmlFor={'selector'} - style={{ - opacity: isEditing ? 0 : 1, - pointerEvents: isEditing ? 'none' : 'all', - transition: '0.4s ease opacity', - }}> - <div - style={{ - width: dimensions, - height: dimensions, - borderRadius: '50%', - background: 'black', - position: 'absolute', - left: this.left, - top: this.top, - }} - /> - <div - style={{ - position: 'absolute', - left: this.left + 8, - top: this.top + 10, - opacity: uploading ? 0 : 1, - transition: '0.4s opacity ease', - }}> - <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} /> - </div> - <img - style={{ - width: 80, - height: 80, - transition: '0.4s opacity ease', - opacity: uploading ? 0.7 : 0, - position: 'absolute', - top: this.top - 15, - left: this.left - 15, - }} - src={'/assets/loading.gif'}></img> - </label> - <input - type={'checkbox'} - onChange={e => runInAction(() => (this.persistent = e.target.checked))} - style={{ - margin: 0, - position: 'absolute', - left: 10, - bottom: 10, - opacity: isEditing || uploading ? 0 : 1, - transition: '0.4s opacity ease', - pointerEvents: isEditing || uploading ? 'none' : 'all', - }} - checked={this.persistent} - onPointerEnter={action(() => (this.removeHover = true))} - onPointerLeave={action(() => (this.removeHover = false))} - /> - <p - style={{ - position: 'absolute', - left: 27, - bottom: 8.4, - fontSize: 12, - opacity: showRemoveLabel ? 1 : 0, - transition: '0.4s opacity ease', - }}> - Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload - </p> - {centerPiece} - <div - style={{ - position: 'absolute', - top: 10, - right: 10, - borderRadius: '50%', - width: 25, - height: 25, - background: 'black', - pointerEvents: uploading ? 'none' : 'all', - opacity: uploading ? 0 : 1, - transition: '0.4s opacity ease', - }} - title={isEditing ? 'Back to Upload' : 'Add Metadata'} - onClick={action(() => (this.editingMetadata = !this.editingMetadata))} - /> - <FontAwesomeIcon - style={{ - pointerEvents: 'none', - position: 'absolute', - right: isEditing ? 14 : 15, - top: isEditing ? 15.4 : 16, - opacity: uploading ? 0 : 1, - transition: '0.4s opacity ease', - }} - icon={isEditing ? 'cloud-upload-alt' : 'tag'} - color="#FFFFFF" - size={'1x'} - /> - <div - style={{ - transition: '0.4s ease opacity', - width: '100%', - height: '100%', - pointerEvents: isEditing ? 'all' : 'none', - opacity: isEditing ? 1 : 0, - overflowY: 'scroll', - }}> - <div - style={{ - borderRadius: '50%', - width: 25, - height: 25, - marginLeft: 10, - position: 'absolute', - right: 41, - top: 10, - }} - title={'Add Metadata Entry'} - onClick={this.addMetadataEntry}> - <FontAwesomeIcon - style={{ - pointerEvents: 'none', - marginLeft: 6.4, - marginTop: 5.2, - }} - icon={'plus'} - size={'1x'} - /> - </div> - <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p> - <hr style={{ margin: '6px 10px 12px 10px' }} /> - {entries.map(doc => ( - <ImportMetadataEntry - Document={doc} - key={doc[Id]} - remove={this.remove} - ref={el => { - if (el) this.entries.push(el); - }} - next={this.addMetadataEntry} - /> - ))} - </div> - </div> - )} - </Measure> - ); - } -} +// render() { +// const dimensions = 50; +// const entries = DocListCast(this.props.Document.data); +// const isEditing = this.editingMetadata; +// const completed = this.completed; +// const quota = this.quota; +// const uploading = this.uploading; +// const showRemoveLabel = this.removeHover; +// const persistent = this.persistent; +// let percent = `${(completed / quota) * 100}`; +// percent = percent.split('.')[0]; +// percent = percent.startsWith('100') ? '99' : percent; +// const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; +// const message = <span className={'phase'}>{this.phase}</span>; +// const centerPiece = this.phase.includes('Google Photos') ? ( +// <img +// src={'/assets/google_photos.png'} +// style={{ +// transition: '0.4s opacity ease', +// width: 30, +// height: 30, +// opacity: uploading ? 1 : 0, +// pointerEvents: 'none', +// position: 'absolute', +// left: 12, +// top: this.top + 10, +// fontSize: 18, +// color: 'white', +// marginLeft: this.left + marginOffset, +// }} +// /> +// ) : ( +// <div +// style={{ +// transition: '0.4s opacity ease', +// opacity: uploading ? 1 : 0, +// pointerEvents: 'none', +// position: 'absolute', +// left: 10, +// top: this.top + 12.3, +// fontSize: 18, +// color: 'white', +// marginLeft: this.left + marginOffset, +// }}> +// {percent}% +// </div> +// ); +// return ( +// <Measure offset onResize={this.preserveCentering}> +// {({ measureRef }) => ( +// <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}> +// {message} +// <input +// id={'selector'} +// ref={this.selector} +// onChange={this.handleSelection} +// type="file" +// style={{ +// position: 'absolute', +// display: 'none', +// }} +// /> +// <label +// htmlFor={'selector'} +// style={{ +// opacity: isEditing ? 0 : 1, +// pointerEvents: isEditing ? 'none' : 'all', +// transition: '0.4s ease opacity', +// }}> +// <div +// style={{ +// width: dimensions, +// height: dimensions, +// borderRadius: '50%', +// background: 'black', +// position: 'absolute', +// left: this.left, +// top: this.top, +// }} +// /> +// <div +// style={{ +// position: 'absolute', +// left: this.left + 8, +// top: this.top + 10, +// opacity: uploading ? 0 : 1, +// transition: '0.4s opacity ease', +// }}> +// <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} /> +// </div> +// <img +// style={{ +// width: 80, +// height: 80, +// transition: '0.4s opacity ease', +// opacity: uploading ? 0.7 : 0, +// position: 'absolute', +// top: this.top - 15, +// left: this.left - 15, +// }} +// src={'/assets/loading.gif'}></img> +// </label> +// <input +// type={'checkbox'} +// onChange={e => runInAction(() => (this.persistent = e.target.checked))} +// style={{ +// margin: 0, +// position: 'absolute', +// left: 10, +// bottom: 10, +// opacity: isEditing || uploading ? 0 : 1, +// transition: '0.4s opacity ease', +// pointerEvents: isEditing || uploading ? 'none' : 'all', +// }} +// checked={this.persistent} +// onPointerEnter={action(() => (this.removeHover = true))} +// onPointerLeave={action(() => (this.removeHover = false))} +// /> +// <p +// style={{ +// position: 'absolute', +// left: 27, +// bottom: 8.4, +// fontSize: 12, +// opacity: showRemoveLabel ? 1 : 0, +// transition: '0.4s opacity ease', +// }}> +// Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload +// </p> +// {centerPiece} +// <div +// style={{ +// position: 'absolute', +// top: 10, +// right: 10, +// borderRadius: '50%', +// width: 25, +// height: 25, +// background: 'black', +// pointerEvents: uploading ? 'none' : 'all', +// opacity: uploading ? 0 : 1, +// transition: '0.4s opacity ease', +// }} +// title={isEditing ? 'Back to Upload' : 'Add Metadata'} +// onClick={action(() => (this.editingMetadata = !this.editingMetadata))} +// /> +// <FontAwesomeIcon +// style={{ +// pointerEvents: 'none', +// position: 'absolute', +// right: isEditing ? 14 : 15, +// top: isEditing ? 15.4 : 16, +// opacity: uploading ? 0 : 1, +// transition: '0.4s opacity ease', +// }} +// icon={isEditing ? 'cloud-upload-alt' : 'tag'} +// color="#FFFFFF" +// size={'1x'} +// /> +// <div +// style={{ +// transition: '0.4s ease opacity', +// width: '100%', +// height: '100%', +// pointerEvents: isEditing ? 'all' : 'none', +// opacity: isEditing ? 1 : 0, +// overflowY: 'scroll', +// }}> +// <div +// style={{ +// borderRadius: '50%', +// width: 25, +// height: 25, +// marginLeft: 10, +// position: 'absolute', +// right: 41, +// top: 10, +// }} +// title={'Add Metadata Entry'} +// onClick={this.addMetadataEntry}> +// <FontAwesomeIcon +// style={{ +// pointerEvents: 'none', +// marginLeft: 6.4, +// marginTop: 5.2, +// }} +// icon={'plus'} +// size={'1x'} +// /> +// </div> +// <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p> +// <hr style={{ margin: '6px 10px 12px 10px' }} /> +// {entries.map(doc => ( +// <ImportMetadataEntry +// Document={doc} +// key={doc[Id]} +// remove={this.remove} +// ref={el => { +// if (el) this.entries.push(el); +// }} +// next={this.addMetadataEntry} +// /> +// ))} +// </div> +// </div> +// )} +// </Measure> +// ); +// } +// } diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 55d37f544..d99828956 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -4,28 +4,33 @@ 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 const ExtractExif = async (document: Doc): Promise<boolean> => { + export type imgInfo = { + contentSize: number; + nativeWidth: number; + nativeHeight: number; + source: string; + exifData: { error: string | undefined; data: string }; + }; + export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => { const field = Cast(document.data, ImageField); - if (!field) { - return false; + return field ? await Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; + }; + + export const AssignImgInfo = (document: Doc, data?: imgInfo) => { + if (data) { + data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth); + const proto = document[DocData]; + const field = Doc.LayoutFieldKey(document); + proto[`${field}_nativeWidth`] = data.nativeWidth; + proto[`${field}_nativeHeight`] = data.nativeHeight; + proto[`${field}_path`] = data.source; + proto[`${field}_exif`] = JSON.stringify(data.exifData.data); + proto[`${field}_contentSize`] = data.contentSize ? data.contentSize : undefined; } - const source = field.url.href; - const { - contentSize, - nativeWidth, - nativeHeight, - exifData: { error, data }, - } = await Networking.PostToServer('/inspectImage', { source }); - document.exif = error || Doc.Get.FromJson({ data }); - const proto = Doc.GetProto(document); - nativeWidth && (document._height = (NumCast(document._width) * nativeHeight) / nativeWidth); - proto['data_nativeWidth'] = nativeWidth; - proto['data_nativeHeight'] = nativeHeight; - proto['data-path'] = source; - proto.data_contentSize = contentSize ? contentSize : undefined; - return data !== undefined; + return document; }; export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => { diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx index 45d8c0c63..58a09b9c9 100644 --- a/src/client/util/Import & Export/ImportMetadataEntry.tsx +++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx @@ -1,10 +1,10 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import { EditableView } from "../../views/EditableView"; -import { action, computed } from "mobx"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Doc } from "../../../fields/Doc"; -import { StrCast, BoolCast } from "../../../fields/Types"; +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { EditableView } from '../../views/EditableView'; +import { action, computed } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Doc } from '../../../fields/Doc'; +import { StrCast, BoolCast } from '../../../fields/Types'; interface KeyValueProps { Document: Doc; @@ -12,19 +12,18 @@ interface KeyValueProps { next: () => void; } -export const keyPlaceholder = "Key"; -export const valuePlaceholder = "Value"; +export const keyPlaceholder = 'Key'; +export const valuePlaceholder = 'Value'; @observer export default class ImportMetadataEntry extends React.Component<KeyValueProps> { - private keyRef = React.createRef<EditableView>(); private valueRef = React.createRef<EditableView>(); private checkRef = React.createRef<HTMLInputElement>(); @computed public get valid() { - return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder); + return this.key.length > 0 && this.key !== keyPlaceholder && this.value.length > 0 && this.value !== valuePlaceholder; } @computed @@ -66,7 +65,7 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps> this.valueRef.current && this.valueRef.current.setIsFocused(true); this.key.length === 0 && (this.key = keyPlaceholder); return true; - } + }; @action updateValue = (newValue: string, shiftDown: boolean) => { @@ -75,68 +74,45 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps> this.value.length > 0 && shiftDown && this.props.next(); this.value.length === 0 && (this.value = valuePlaceholder); return true; - } + }; render() { const keyValueStyle: React.CSSProperties = { paddingLeft: 10, - width: "50%", + width: '50%', opacity: this.valid ? 1 : 0.5, }; return ( <div style={{ - display: "flex", - flexDirection: "row", + display: 'flex', + flexDirection: 'row', paddingBottom: 5, paddingRight: 5, - justifyContent: "center", - alignItems: "center", - alignContent: "center" - }} - > - <input - onChange={e => this.onDataDoc = e.target.checked} - ref={this.checkRef} - style={{ margin: "0 10px 0 15px" }} - type="checkbox" - title={"Add to Data Document?"} - checked={this.onDataDoc} - /> - <div className={"key_container"} style={keyValueStyle}> - <EditableView - ref={this.keyRef} - contents={this.key} - SetValue={this.updateKey} - GetValue={() => ""} - oneLine={true} - /> + justifyContent: 'center', + alignItems: 'center', + alignContent: 'center', + }}> + <input onChange={e => (this.onDataDoc = e.target.checked)} ref={this.checkRef} style={{ margin: '0 10px 0 15px' }} type="checkbox" title={'Add to Data Document?'} checked={this.onDataDoc} /> + <div className={'key_container'} style={keyValueStyle}> + <EditableView ref={this.keyRef} contents={this.key} SetValue={this.updateKey} GetValue={() => ''} oneLine={true} /> </div> - <div - className={"value_container"} - style={keyValueStyle}> - <EditableView - ref={this.valueRef} - contents={this.value} - SetValue={this.updateValue} - GetValue={() => ""} - oneLine={true} - /> + <div className={'value_container'} style={keyValueStyle}> + <EditableView ref={this.valueRef} contents={this.value} SetValue={this.updateValue} GetValue={() => ''} oneLine={true} /> </div> - <div onClick={() => this.props.remove(this)} title={"Delete Entry"}> + <div onClick={() => this.props.remove(this)} title={'Delete Entry'}> <FontAwesomeIcon - icon={"plus"} - color={"red"} - size={"1x"} + icon={'plus'} + color={'red'} + size={'1x'} style={{ marginLeft: 15, marginRight: 15, - transform: "rotate(45deg)" + transform: 'rotate(45deg)', }} /> </div> </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index be885312d..a2f5826fe 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; import { GestureUtils } from '../../pen-gestures/GestureUtils'; import { Utils } from '../../Utils'; import './InteractionUtils.scss'; diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 146eed6c2..20261859c 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -1,14 +1,16 @@ -import { action, observable, runInAction } from 'mobx'; +import { action, runInAction } from 'mobx'; import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; -import { DocFocusOptions, OpenWhere } from '../views/nodes/DocumentView'; +import { OpenWhere } from '../views/nodes/DocumentView'; +import { FocusViewOptions } from '../views/nodes/FieldView'; import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; +import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; /* * link doc: @@ -23,19 +25,18 @@ import { UndoManager } from './UndoManager'; * - user defined kvps */ export class LinkFollower { - @observable public static IsFollowing = false; // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => { const batch = UndoManager.StartBatch('Follow Link'); - runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value + runInAction(() => SnappingManager.SetIsLinkFollowing(true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value return LinkFollower.traverseLink( linkDoc, sourceDoc, action(() => { batch.end(); - Doc.AddUnHighlightWatcher(action(() => (LinkFollower.IsFollowing = false))); + Doc.AddUnHighlightWatcher(() => SnappingManager.SetIsLinkFollowing(false)); }), altKey ? true : undefined ); @@ -63,16 +64,16 @@ export class LinkFollower { sourceDoc === linkDoc.link_anchor_1 ? linkDoc.link_anchor_2 : sourceDoc === linkDoc.link_anchor_2 - ? linkDoc.link_anchor_1 - : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc) - ? linkDoc.link_anchor_2 - : linkDoc.link_anchor_1 + ? linkDoc.link_anchor_1 + : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc) + ? linkDoc.link_anchor_2 + : linkDoc.link_anchor_1 ) as Doc; const srcAnchor = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc; if (target) { const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); - const options: DocFocusOptions = { + const options: FocusViewOptions = { playAudio: BoolCast(srcAnchor.followLinkAudio), playMedia: BoolCast(srcAnchor.followLinkVideo), toggleTarget, diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index ba53a760f..353f28a92 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,7 +1,7 @@ -import { action, observable, observe, runInAction } from 'mobx'; +import { action, makeObservable, observable, observe, runInAction } from 'mobx'; import { computedFn } from 'mobx-utils'; import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; -import { DirectLinks } from '../../fields/DocSymbols'; +import { DirectLinks, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; import { List } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; @@ -22,9 +22,9 @@ import { ScriptingGlobals } from './ScriptingGlobals'; */ export class LinkManager { @observable static _instance: LinkManager; - @observable static userLinkDBs: Doc[] = []; - @observable public static currentLink: Opt<Doc>; - @observable public static currentLinkAnchor: Opt<Doc>; + @observable.shallow userLinkDBs: Doc[] = []; + @observable public static currentLink: Opt<Doc> = undefined; + @observable public static currentLinkAnchor: Opt<Doc> = undefined; public static get Instance() { return LinkManager._instance; } @@ -32,21 +32,20 @@ export class LinkManager { public static Links(doc: Doc | undefined) { return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : []; } - public static addLinkDB = async (linkDb: any) => { + public addLinkDB = async (linkDb: any) => { await Promise.all( ((await DocListCastAsync(linkDb.data)) ?? []).map(link => // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager [PromiseValue(link?.link_anchor_1), PromiseValue(link?.link_anchor_2)] ) ); - LinkManager.userLinkDBs.push(linkDb); + this.userLinkDBs.push(linkDb); }; public static AutoKeywords = 'keywords:Usages'; - static _links: Doc[] = []; constructor() { + makeObservable(this); LinkManager._instance = this; this.createlink_relationshipLists(); - LinkManager.userLinkDBs = []; // 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 // Then add the link to the anchor protos. @@ -59,8 +58,8 @@ export class LinkManager { link && action(lAnchProtoProtos => { Doc.AddDocToList(Doc.UserDoc(), 'links', link); - lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link); - lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link); + lAnchs[0] && lAnchs[0][DocData][DirectLinks].add(link); + lAnchs[1] && lAnchs[1][DocData][DirectLinks].add(link); }) ) ) @@ -75,8 +74,8 @@ export class LinkManager { Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) => Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( action(lAnchProtoProtos => { - link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].delete(link); - link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].delete(link); + link && lAnchs[0] && lAnchs[0][DocData][DirectLinks].delete(link); + link && lAnchs[1] && lAnchs[1][DocData][DirectLinks].delete(link); }) ) ) @@ -85,7 +84,6 @@ export class LinkManager { ); const watchUserLinkDB = (userLinkDBDoc: Doc) => { - LinkManager._links.push(...DocListCast(userLinkDBDoc.data)); const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields if (userLinkDBDoc.data) { observe( @@ -124,7 +122,7 @@ export class LinkManager { } }; observe( - LinkManager.userLinkDBs, + this.userLinkDBs, change => { switch (change.type as any) { case 'splice': @@ -135,8 +133,8 @@ export class LinkManager { }, true ); - runInAction(() => (FieldLoader.ServerLoadStatus.message = 'links')); - LinkManager.addLinkDB(Doc.LinkDBDoc()); + FieldLoader.ServerLoadStatus.message = 'links'; + this.addLinkDB(Doc.LinkDBDoc()); } public createlink_relationshipLists = () => { @@ -163,8 +161,8 @@ export class LinkManager { public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { - return Array.from(Doc.GetProto(anchor)[DirectLinks]); + public getAllDirectLinks(anchor?: Doc): Doc[] { + return anchor ? Array.from(anchor[DirectLinks]) : []; } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { @@ -197,14 +195,32 @@ export class LinkManager { } // finds the opposite anchor of a given anchor in a link - //TODO This should probably return undefined if there isn't an opposite anchor - //TODO This should also await the return value of the anchor so we don't filter out promises public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { - const a1 = Cast(linkDoc.link_anchor_1, Doc, null); - const a2 = Cast(linkDoc.link_anchor_2, Doc, null); - if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return a2; - if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return a1; - if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; + const id = LinkManager.anchorIndex(linkDoc, anchor); + const a1 = DocCast(linkDoc.link_anchor_1); + const a2 = DocCast(linkDoc.link_anchor_2); + return id === '1' ? a2 : id === '2' ? a1 : id === '0' ? linkDoc : undefined; + // if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return a2; + // if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return a1; + // if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; + } + public static anchorIndex(linkDoc: Doc, anchor: Doc) { + const a1 = DocCast(linkDoc.link_anchor_1); + const a2 = DocCast(linkDoc.link_anchor_2); + if (linkDoc.link_matchEmbeddings) { + return [a2, a2.annotationOn].includes(anchor) ? '2' : '1'; + } + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1'; + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2'; + if (Doc.AreProtosEqual(anchor, linkDoc)) return '0'; + + // const a1 = DocCast(linkDoc.link_anchor_1); + // const a2 = DocCast(linkDoc.link_anchor_2); + // if (linkDoc.link_matchEmbeddings) { + // return [a2, a2.annotationOn].includes(anchor) ? '2' : '1'; + // } + // if (Doc.AreProtosEqual(a2, anchor) || Doc.AreProtosEqual(a2.annotationOn as Doc, anchor)) return '2'; + // return Doc.AreProtosEqual(a1, anchor) || Doc.AreProtosEqual(a1.annotationOn as Doc, anchor) ? '1' : '2'; } } diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts index 4dd2fcd35..7638e2ce0 100644 --- a/src/client/util/PingManager.ts +++ b/src/client/util/PingManager.ts @@ -1,14 +1,23 @@ -import { action, observable, runInAction } from 'mobx'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import { Networking } from '../Network'; import { CurrentUserUtils } from './CurrentUserUtils'; export class PingManager { // create static instance and getter for global use @observable static _instance: PingManager; + @observable IsBeating = true; static get Instance(): PingManager { return PingManager._instance; } - @observable IsBeating = true; + // not used now, but may need to clear interval + private _interval: NodeJS.Timeout | null = null; + INTERVAL_SECONDS = 1; + constructor() { + makeObservable(this); + PingManager._instance = this; + this._interval = setInterval(this.sendPing, this.INTERVAL_SECONDS * 1000); + } + private setIsBeating = action((status: boolean) => { this.IsBeating = status; setTimeout(this.showAlert, 100); @@ -28,12 +37,4 @@ export class PingManager { } } }; - - // not used now, but may need to clear interval - private _interval: NodeJS.Timeout | null = null; - INTERVAL_SECONDS = 1; - constructor() { - PingManager._instance = this; - this._interval = setInterval(this.sendPing, this.INTERVAL_SECONDS * 1000); - } } diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx index c8940194c..f96d8a5df 100644 --- a/src/client/util/RTFMarkup.tsx +++ b/src/client/util/RTFMarkup.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { MainViewModal } from '../views/MainViewModal'; @@ -23,10 +23,11 @@ export class RTFMarkup extends React.Component<{}> { constructor(props: {}) { super(props); + makeObservable(this); RTFMarkup.Instance = this; } - @observable _stats: { [key: string]: any } | undefined; + @observable _stats: { [key: string]: any } | undefined = undefined; /** * @returns the main interface of the SharingManager. diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts index d99630f82..b881f18b4 100644 --- a/src/client/util/ReplayMovements.ts +++ b/src/client/util/ReplayMovements.ts @@ -1,4 +1,4 @@ -import { IReactionDisposer, observable, reaction } from 'mobx'; +import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { Doc, IdToDoc } from '../../fields/Doc'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; @@ -19,6 +19,7 @@ export class ReplayMovements { return ReplayMovements._instance; } constructor() { + makeObservable(this); // init the global instance ReplayMovements._instance = this; diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 70c2e3842..dfaacf318 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -120,6 +120,7 @@ class ScriptingCompilerHost { } return undefined; } + // getDefaultLibFileName(options: ts.CompilerOptions): string { getDefaultLibFileName(options: any): string { return 'node_modules/typescript/lib/lib.d.ts'; // No idea what this means... @@ -159,7 +160,7 @@ class ScriptingCompilerHost { export type Traverser = (node: ts.Node, indentation: string) => boolean | void; export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { - transformer: ts.TransformerFactory<ts.SourceFile>; + transformer: ts.TransformerFactory<ts.Node>; getVars?: () => { [name: string]: Field }; }; export interface ScriptOptions { @@ -219,7 +220,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, }); - script = printer.printFile(transformed[0]); + script = printer.printFile(transformed[0].getSourceFile()); } result.dispose(); } @@ -247,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile('file.ts', funcScript); - if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib.default); const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); const outputText = host.readFile('file.js'); @@ -264,6 +265,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp } ScriptingGlobals.add(CompileScript); -ScriptingGlobals.add(function runScript(self: Doc, script: ScriptField) { - return script?.script.run({ this: self, self: self }).result; +ScriptingGlobals.add(function runScript(doc: Doc, script: ScriptField) { + return script?.script.run({ this: doc }).result; }); diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx index d4620ae3f..785526ab3 100644 --- a/src/client/util/ScrollBox.tsx +++ b/src/client/util/ScrollBox.tsx @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; export class ScrollBox extends React.Component<React.PropsWithChildren<{}>> { onWheel = (e: React.WheelEvent) => { diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 560d6b30f..2cc64f415 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -1,15 +1,12 @@ -import * as rp from 'request-promise'; -import { DocServer } from '../DocServer'; import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Utils } from '../../Utils'; -import { DocumentType } from '../documents/DocumentTypes'; import { StrCast } from '../../fields/Types'; +import { DocumentType } from '../documents/DocumentTypes'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; - export function SearchCollection(rootDoc: Opt<Doc>, query: string) { + export function SearchCollection(collectionDoc: Opt<Doc>, query: string) { const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = [ 'x', @@ -48,8 +45,8 @@ export namespace SearchUtil { query = query.toLowerCase(); const results = new Map<Doc, string[]>(); - if (rootDoc) { - const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]); + if (collectionDoc) { + const docs = DocListCast(collectionDoc[Doc.LayoutFieldKey(collectionDoc)]); const docIDs: String[] = []; SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { const dtype = StrCast(doc.type) as DocumentType; @@ -110,154 +107,4 @@ export namespace SearchUtil { depth++; } } - export interface IdSearchResult { - ids: string[]; - lines: string[][]; - numFound: number; - highlighting: HighlightingResult | undefined; - } - - export interface DocSearchResult { - docs: Doc[]; - lines: string[][]; - numFound: number; - highlighting: HighlightingResult | undefined; - } - - export interface SearchParams { - hl?: string; - 'hl.fl'?: string; - start?: number; - rows?: number; - fq?: string; - sort?: string; - allowEmbeddings?: boolean; - onlyEmbeddings?: boolean; - facet?: string; - 'facet.field'?: string; - } - export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>; - export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>; - export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { - query = query || '*'; //If we just have a filter query, search for * as the query - const rpquery = Utils.prepend('/dashsearch'); - let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`); - if (options.onlyEmbeddings) { - const header = query.match(/_[atnb]?:/) ? replacedQuery : 'DEFAULT:' + replacedQuery; - replacedQuery = `{!join from=id to=proto_i}* AND ${header}`; - } - //console.log("Q: " + replacedQuery + " fq: " + options.fq); - const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } }); - const result: IdSearchResult = gotten.startsWith('<') ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); - if (!returnDocs) { - return result; - } - - const { ids, highlighting } = result; - - const txtresult = - query !== '*' && - JSON.parse( - await rp.get(Utils.prepend('/textsearch'), { - qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, '') }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures - }) - ); - - const fileids = txtresult ? txtresult.ids : []; - const newIds: string[] = []; - const newLines: string[][] = []; - // bcz: we stopped storing fileUpload id's, so this won't find anything - // if (fileids) { - // await Promise.all( - // fileids.map(async (tr: string, i: number) => { - // const docQuery = 'fileUpload_t:' + tr.substr(0, 7); //If we just have a filter query, search for * as the query - // const docResult = JSON.parse(await rp.get(Utils.prepend('/dashsearch'), { qs: { ...options, q: docQuery } })); - // newIds.push(...docResult.ids); - // newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); - // }) - // ); - // } - - const theDocs: Doc[] = []; - const theLines: string[][] = []; - const textDocMap = await DocServer.GetRefFields(newIds); - const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc); - for (let i = 0; i < textDocs.length; i++) { - const testDoc = textDocs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) { - theDocs.push(Doc.GetProto(testDoc)); - theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase()))); - } - } - - const docMap = await DocServer.GetRefFields(ids); - const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc); - for (let i = 0; i < ids.length; i++) { - const testDoc = docs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowEmbeddings || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { - theDocs.push(testDoc); - theLines.push([]); - } else { - result.numFound--; - } - } - - return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines }; - } - - export async function GetEmbeddingsOfDocument(doc: Doc): Promise<Doc[]>; - export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs: false): Promise<string[]>; - export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> { - const proto = Doc.GetProto(doc); - const protoId = proto[Id]; - if (returnDocs) { - return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).docs; - } else { - return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).ids; - } - // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); - } - - export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { - const results = await Search('', true, { fq: `proto_i:"${doc[Id]}"` }); - return results.docs; - } - - export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[]; embeddingContexts: Doc[] }> { - const docContexts = (await Search('', true, { fq: `data_l:"${doc[Id]}"` })).docs; - const embeddings = await GetEmbeddingsOfDocument(doc, false); - const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', true, { fq: `data_l:"${doc}"` }))); - const contexts = { contexts: docContexts, embeddingContexts: [] as Doc[] }; - embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.docs)); - return contexts; - } - - export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[]; embeddingContexts: string[] }> { - const docContexts = (await Search('', false, { fq: `data_l:"${doc[Id]}"` })).ids; - const embeddings = await GetEmbeddingsOfDocument(doc, false); - const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', false, { fq: `data_l:"${doc}"` }))); - const contexts = { contexts: docContexts, embeddingContexts: [] as string[] }; - embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.ids)); - return contexts; - } - - export async function GetAllDocs() { - const query = '*'; - const response = await rp.get(Utils.prepend('/dashsearch'), { - qs: { start: 0, rows: 10000, q: query }, - }); - const result: IdSearchResult = JSON.parse(response); - const { ids, numFound, highlighting } = result; - const docMap = await DocServer.GetRefFields(ids); - const docs: Doc[] = []; - for (const id of ids) { - const field = docMap[id]; - if (field instanceof Doc) { - docs.push(field); - } - } - return docs; - // const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); - // return docs as Doc[]; - } } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index d0f66d124..f2a327445 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,6 +1,6 @@ -import { action, observable, ObservableMap } from 'mobx'; -import { computedFn } from 'mobx-utils'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import { Doc, Opt } from '../../fields/Doc'; +import { DocViews } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; import { Cast, DocCast } from '../../fields/Types'; @@ -10,117 +10,70 @@ import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; import { UndoManager } from './UndoManager'; -export namespace SelectionManager { - class Manager { - @observable IsDragging: boolean = false; - SelectedViewsMap: ObservableMap<DocumentView, Doc> = new ObservableMap(); - @observable SelectedViews: DocumentView[] = []; - @observable SelectedSchemaDocument: Doc | undefined; - - @action - SelectSchemaViewDoc(doc: Opt<Doc>) { - manager.SelectedSchemaDocument = doc; - } - @action - SelectView(docView: DocumentView, ctrlPressed: boolean): void { - // if doc is not in SelectedDocuments, add it - if (!manager.SelectedViewsMap.get(docView)) { - if (!ctrlPressed) { - this.DeselectAll(); - } - - manager.SelectedViews.push(docView); - manager.SelectedViewsMap.set(docView, docView.rootDoc); - docView.props.whenChildContentsActiveChanged(true); - } else if (!ctrlPressed && (Array.from(manager.SelectedViewsMap.entries()).length > 1 || manager.SelectedSchemaDocument)) { - Array.from(manager.SelectedViewsMap.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false)); - manager.SelectedSchemaDocument = undefined; - manager.SelectedViews.length = 0; - manager.SelectedViewsMap.clear(); - manager.SelectedViews.push(docView); - manager.SelectedViewsMap.set(docView, docView.rootDoc); - } - } - @action - DeselectView(docView?: DocumentView): void { - if (docView && manager.SelectedViewsMap.get(docView)) { - manager.SelectedViewsMap.delete(docView); - manager.SelectedViews.splice(manager.SelectedViews.indexOf(docView), 1); - docView.props.whenChildContentsActiveChanged(false); - } - } - @action - DeselectAll(): void { - LinkManager.currentLink = undefined; - LinkManager.currentLinkAnchor = undefined; - manager.SelectedSchemaDocument = undefined; - Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false)); - manager.SelectedViewsMap.clear(); - manager.SelectedViews.length = 0; - } +export class SelectionManager { + private static _manager: SelectionManager; + private static get Instance() { + return SelectionManager._manager ?? new SelectionManager(); } - const manager = new Manager(); + @observable.shallow SelectedViews: DocumentView[] = []; + @observable IsDragging: boolean = false; + @observable SelectedSchemaDocument: Doc | undefined = undefined; - export function DeselectView(docView?: DocumentView): void { - manager.DeselectView(docView); - } - export function SelectView(docView: DocumentView | undefined, ctrlPressed: boolean): void { - if (!docView) DeselectAll(); - else manager.SelectView(docView, ctrlPressed); - } - export function SelectSchemaViewDoc(document: Opt<Doc>, deselectAllFirst?: boolean): void { - if (deselectAllFirst) manager.DeselectAll(); - manager.SelectSchemaViewDoc(document); + private constructor() { + SelectionManager._manager = this; + makeObservable(this); } - const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { - // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed - return manager.SelectedViewsMap.get(doc) ? true : false; + @action + public static SelectSchemaViewDoc = (doc: Opt<Doc>, deselectAllFirst?: boolean) => { + if (deselectAllFirst) this.DeselectAll(); + this.Instance.SelectedSchemaDocument = doc; + }; + + public static SelectView = action((docView: DocumentView | undefined, extendSelection: boolean): void => { + if (!docView) this.DeselectAll(); + else if (!docView.IsSelected) { + if (!extendSelection) this.DeselectAll(); + this.Instance.SelectedViews.push(docView); + docView.IsSelected = true; + docView._props.whenChildContentsActiveChanged(true); + } }); - // computed functions, such as used in IsSelected generate errors if they're called outside of a - // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature - // to avoid unnecessary mobx invalidations when running inside a reaction. - export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean { - return !doc - ? false - : outsideReaction - ? manager.SelectedViewsMap.get(doc) - ? true - : false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() - : IsSelectedCache(doc); - } - export function DeselectAll(except?: Doc): void { - let found: DocumentView | undefined = undefined; - if (except) { - for (const view of Array.from(manager.SelectedViewsMap.keys())) { - if (view.props.Document === except) found = view; - } + public static DeselectView = action((docView?: DocumentView): void => { + if (docView && this.Instance.SelectedViews.includes(docView)) { + docView.IsSelected = false; + this.Instance.SelectedViews.splice(this.Instance.SelectedViews.indexOf(docView), 1); + docView._props.whenChildContentsActiveChanged(false); } + }); - manager.DeselectAll(); - if (found) manager.SelectView(found, false); - } + public static DeselectAll = (except?: Doc): void => { + const found = this.Instance.SelectedViews.find(dv => dv.Document === except); + LinkManager.currentLink = undefined; + LinkManager.currentLinkAnchor = undefined; + runInAction(() => (this.Instance.SelectedSchemaDocument = undefined)); + this.Instance.SelectedViews.forEach(dv => { + dv.IsSelected = false; + dv._props.whenChildContentsActiveChanged(false); + }); + runInAction(() => (this.Instance.SelectedViews.length = 0)); + if (found) this.SelectView(found, false); + }; - export function Views(): Array<DocumentView> { - return manager.SelectedViews; - // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._type_collection !== CollectionViewType.Docking); - } - export function SelectedSchemaDoc(): Doc | undefined { - return manager.SelectedSchemaDocument; - } - export function Docs(): Doc[] { - return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._type_collection !== CollectionViewType.Docking); - // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._type_collection !== CollectionViewType.Docking); - } + public static IsSelected = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected); + public static get Views() { return this.Instance.SelectedViews; } // prettier-ignore + public static get SelectedSchemaDoc() { return this.Instance.SelectedSchemaDocument; } // prettier-ignore + public static get Docs() { return this.Instance.SelectedViews.map(dv => dv.Document).filter(doc => doc?._type_collection !== CollectionViewType.Docking); } // prettier-ignore } + ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) { if (Doc.noviceMode && expertMode) return false; if (type === 'tab') { - return SelectionManager.Views().lastElement()?.props.renderDepth === 0; + return SelectionManager.Views.lastElement()?._props.renderDepth === 0; } - let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); + let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc ?? SelectionManager.Docs.lastElement()); return selected?.type === type || selected?.type_collection === type || !type; }); ScriptingGlobals.add(function deselectAll() { @@ -145,8 +98,8 @@ ScriptingGlobals.add(function redo() { return UndoManager.Redo(); }); ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { - const docs = SelectionManager.Views() - .map(dv => dv.props.Document) - .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); + const docs = SelectionManager.Views.map(dv => dv.Document).filter( + d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)) + ); return docs.length ? new List(docs) : prevValue; }); diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 76037a7e9..8daa69890 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,6 +1,5 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr'; import { Field } from '../../fields/Doc'; -import { ClientUtils } from './ClientUtils'; let serializing = 0; export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) { diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx index 08dbaac5d..c8df9182d 100644 --- a/src/client/util/ServerStats.tsx +++ b/src/client/util/ServerStats.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { MainViewModal } from '../views/MainViewModal'; @@ -33,10 +33,11 @@ export class ServerStats extends React.Component<{}> { constructor(props: {}) { super(props); + makeObservable(this); ServerStats.Instance = this; } - @observable _stats: { [key: string]: any } | undefined; + @observable _stats: { [key: string]: any } | undefined = undefined; /** * @returns the main interface of the SharingManager. @@ -56,9 +57,7 @@ export class ServerStats extends React.Component<{}> { <br /> <span>Active users:{this._stats?.socketMap.length}</span> - {this._stats?.socketMap.map((user: any) => ( - <p>{user.username}</p> - ))} + {this._stats?.socketMap.map((user: any) => <p>{user.username}</p>)} </div> </div> ); diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index bca649bc3..dbfc48c63 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,4 +1,4 @@ -@import '../views/global/globalCssVariables'; +@import '../views/global/globalCssVariables.module'; .settings-interface { //background-color: whitesmoke !important; @@ -187,14 +187,12 @@ display: flex; flex-direction: column; - .close-button { position: absolute; right: 2px; top: 2px; } - .settings-content { padding: 10px; width: 500px; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index f75322905..5bf9e5b00 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,18 +1,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, 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 { Doc } from '../../fields/Doc'; +import { Utils, addStyleSheet, addStyleSheetRule } from '../../Utils'; +import { Doc, Opt } from '../../fields/Doc'; import { DashVersion } from '../../fields/DocSymbols'; import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types'; -import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; -import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Networking } from '../Network'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; +import { GestureOverlay } from '../views/GestureOverlay'; import { MainViewModal } from '../views/MainViewModal'; +import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; import { undoBatch } from './UndoManager'; @@ -41,13 +43,14 @@ export class SettingsManager extends React.Component<{}> { @observable private curr_password = ''; @observable private new_password = ''; @observable private new_confirm = ''; + @observable private _lastPressedSidebarBtn: Opt<Doc> = undefined; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach @observable activeTab = 'Accounts'; - @observable public static propertiesWidth: number = 0; - @observable public static headerBarHeight: number = 0; + @observable public propertiesWidth: number = 0; constructor(props: {}) { super(props); + makeObservable(this); SettingsManager.Instance = this; this.matchSystem(); window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { @@ -91,6 +94,9 @@ export class SettingsManager extends React.Component<{}> { return StrCast(Doc.UserDoc().userBackgroundColor); } + public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore + public SetLastPressedBtn = (state?:Doc) => runInAction(() => (this._lastPressedSidebarBtn = state)); // prettier-ignore + @undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice')); @undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_showTitle = (e.currentTarget as any).value ? 'title' : undefined)); @undoBatch changeFontFamily = action((font: string) => (Doc.UserDoc().fontFamily = font)); @@ -114,7 +120,6 @@ export class SettingsManager extends React.Component<{}> { }); @undoBatch - @action changeColorScheme = action((scheme: string) => { Doc.UserDoc().userTheme = scheme; switch (scheme) { @@ -228,8 +233,8 @@ export class SettingsManager extends React.Component<{}> { formLabel={'Show Button Labels'} formLabelPlacement={'right'} toggleType={ToggleType.SWITCH} - onClick={e => Doc.SetShowIconLabels(!Doc.GetShowIconLabels())} - toggleStatus={Doc.GetShowIconLabels()} + onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)} + toggleStatus={FontIconBox.ShowIconLabels} size={Size.XSMALL} color={SettingsManager.userColor} /> @@ -237,8 +242,8 @@ export class SettingsManager extends React.Component<{}> { formLabel={'Recognize Ink Gestures'} formLabelPlacement={'right'} toggleType={ToggleType.SWITCH} - onClick={e => Doc.SetRecognizeGestures(!Doc.GetRecognizeGestures())} - toggleStatus={Doc.GetRecognizeGestures()} + onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)} + toggleStatus={GestureOverlay.RecognizeGestures} size={Size.XSMALL} color={SettingsManager.userColor} /> @@ -448,7 +453,7 @@ export class SettingsManager extends React.Component<{}> { val: freeformScrollMode.Zoom, }, ]} - selectedVal={StrCast(Doc.UserDoc().freeformScrollMode)} + selectedVal={StrCast(Doc.UserDoc().freeformScrollMode, 'zoom')} setSelectedVal={val => this.setFreeformScrollMode(val as string)} dropdownType={DropdownType.SELECT} type={Type.TERT} diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 8d59426ec..fddf735e3 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { concat, intersection } from 'lodash'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; @@ -67,8 +67,8 @@ export class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @observable private isOpen = false; // whether the SharingManager modal is open or not @observable public users: ValidatedUser[] = []; // the list of users with sharing docs - @observable private targetDoc: Doc | undefined; // the document being shared - @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared + @observable private targetDoc: Doc | undefined = undefined; // the document being shared + @observable private targetDocView: DocumentView | undefined = undefined; // the DocumentView of the document being shared // @observable private copied = false; @observable private dialogueBoxOpacity = 1; // for the modal @observable private overlayOpacity = 0.4; // for the modal @@ -94,7 +94,7 @@ export class SharingManager extends React.Component<{}> { this.populateUsers(); runInAction(() => { this.targetDocView = target; - this.targetDoc = target_doc || target?.props.Document; + this.targetDoc = target_doc || target?.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; this.permissions = SharingPermissions.Augment; @@ -119,6 +119,7 @@ export class SharingManager extends React.Component<{}> { constructor(props: {}) { super(props); + makeObservable(this); SharingManager.Instance = this; } @@ -133,7 +134,7 @@ export class SharingManager extends React.Component<{}> { * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. */ populateUsers = async () => { - if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') { + 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); @@ -162,7 +163,7 @@ export class SharingManager extends React.Component<{}> { const { user, sharingDoc } = recipient; const target = targetDoc || this.targetDoc!; const acl = `acl-${normalizeEmail(user.email)}`; - const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc); + const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); if (permission !== SharingPermissions.None) { @@ -180,7 +181,7 @@ export class SharingManager extends React.Component<{}> { const target = targetDoc || this.targetDoc!; const acl = `acl-${normalizeEmail(StrCast(group.title))}`; - const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc); + const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); @@ -317,7 +318,7 @@ export class SharingManager extends React.Component<{}> { private focusOn = (contents: string) => { const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; - const docs = SelectionManager.Views().length > 1 ? SelectionManager.Views().map(docView => docView.props.Document) : [this.targetDoc]; + const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.Document) : [this.targetDoc]; return ( <span className="focus-span" @@ -444,7 +445,7 @@ export class SharingManager extends React.Component<{}> { 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.rootDoc); + let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); if (this.myDocAcls) { const newDocs: Doc[] = []; diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 3cb41ab4d..40c3f76fb 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,47 +1,47 @@ -import { observable, action, runInAction } from 'mobx'; -import { Doc } from '../../fields/Doc'; +import { observable, action, runInAction, reaction, makeObservable } from 'mobx'; +import { Doc, Opt } from '../../fields/Doc'; -export namespace SnappingManager { - class Manager { - @observable IsDragging: boolean = false; - @observable IsResizing: Doc | undefined; - @observable public horizSnapLines: number[] = []; - @observable public vertSnapLines: number[] = []; - @action public clearSnapLines() { - this.vertSnapLines = []; - this.horizSnapLines = []; - } - @action public addSnapLines(horizLines: number[], vertLines: number[]) { - this.horizSnapLines.push(...horizLines); - this.vertSnapLines.push(...vertLines); - } +export class SnappingManager { + private static _manager: SnappingManager; + private static get Instance() { + return SnappingManager._manager ?? new SnappingManager(); } - const manager = new Manager(); + @observable _shiftKey = false; + @observable _ctrlKey = false; + @observable _isLinkFollowing = false; + @observable _isDragging: boolean = false; + @observable _isResizing: Doc | undefined = undefined; + @observable _canEmbed: boolean = false; + @observable _horizSnapLines: number[] = []; + @observable _vertSnapLines: number[] = []; + @observable _exploreMode = false; - export function clearSnapLines() { - manager.clearSnapLines(); - } - export function addSnapLines(horizLines: number[], vertLines: number[]) { - manager.addSnapLines(horizLines, vertLines); - } - export function horizSnapLines() { - return manager.horizSnapLines; - } - export function vertSnapLines() { - return manager.vertSnapLines; + private constructor() { + SnappingManager._manager = this; + makeObservable(this); } - export function SetIsDragging(dragging: boolean) { - runInAction(() => (manager.IsDragging = dragging)); - } - export function SetIsResizing(doc: Doc | undefined) { - runInAction(() => (manager.IsResizing = doc)); - } - export function GetIsDragging() { - return manager.IsDragging; - } - export function GetIsResizing() { - return manager.IsResizing; - } + @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); + }; + + public static get HorizSnapLines() { return this.Instance._horizSnapLines; } // prettier-ignore + public static get VertSnapLines() { return this.Instance._vertSnapLines; } // prettier-ignore + public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore + public static get CtrlKey() { return this.Instance._ctrlKey; } // 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 + 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 SetIsLinkFollowing= (follow: boolean) => runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore + public static SetIsDragging = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore + public static SetIsResizing = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore + public static SetCanEmbed = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore + public static SetExploreMode = (state:boolean) => runInAction(() => (this.Instance._exploreMode = state)); // prettier-ignore } diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts index 0e56ee1bc..f9c2d522f 100644 --- a/src/client/util/TrackMovements.ts +++ b/src/client/util/TrackMovements.ts @@ -1,4 +1,4 @@ -import { IReactionDisposer, observable, observe, reaction } from 'mobx'; +import { IReactionDisposer, makeObservable, observable, observe, reaction } from 'mobx'; import { NumCast } from '../../fields/Types'; import { Doc, DocListCast } from '../../fields/Doc'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; @@ -40,7 +40,7 @@ export class TrackMovements { constructor() { // init the global instance TrackMovements._instance = this; - + makeObservable(this); // init the instance variables this.currentPresentation = TrackMovements.NULL_PRESENTATION; this.tracking = false; @@ -126,12 +126,12 @@ export class TrackMovements { } // init the dispose funcs on the page - const docList = DocListCast(CollectionDockingView.Instance?.props.Document.data); + const docList = DocListCast(CollectionDockingView.Instance?.Document.data); this.updateRecordingFFViewsFromTabs(docList); // create a reaction to monitor changes in tabs this.tabChangeDisposeFunc = reaction( - () => CollectionDockingView.Instance?.props.Document.data, + () => CollectionDockingView.Instance?.Document.data, change => { // TODO: consider changing between dashboards // console.info('change in tabs', change); diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts index e9170ec36..dca37c960 100644 --- a/src/client/util/Transform.ts +++ b/src/client/util/Transform.ts @@ -2,65 +2,105 @@ export class Transform { private _translateX: number = 0; private _translateY: number = 0; private _scale: number = 1; + private _rotate: number = 0; static Identity(): Transform { return new Transform(0, 0, 1); } - get TranslateX(): number { return this._translateX; } - get TranslateY(): number { return this._translateY; } - get Scale(): number { return this._scale; } + get TranslateX(): number { + return this._translateX; + } + get TranslateY(): number { + return this._translateY; + } + get Scale(): number { + return this._scale; + } + get Rotate(): number { + return this._rotate; + } + get RotateDeg(): number { + return (this._rotate * 180) / Math.PI; + } - constructor(x: number, y: number, scale: number) { + /** + * Represents a transformation/scale matrix (can contain a rotation value, but it is not used when transforming points) + * @param x + * @param y + * @param scale + * @param rotation NOTE: this is passed along but is NOT used by any of the transformation functionsStores + */ + constructor(x: number, y: number, scale: number, rotationRadians?: number) { this._translateX = x; this._translateY = y; this._scale = scale; + this._rotate = rotationRadians ?? 0; } + /** + * Rotate in radians + * @param rot + * @returns the modified transformation + */ + rotate = (rot: number): this => { + this._rotate += rot; + return this; + }; + /** + * Rotation in degrees + * @param rot + * @returns the modified transformation + */ + rotateDeg = (rot: number): this => { + this._rotate += (rot * Math.PI) / 180; + return this; + }; + translate = (x: number, y: number): this => { this._translateX += x; this._translateY += y; return this; - } + }; scale = (scale: number): this => { this._scale *= scale; this._translateX *= scale; this._translateY *= scale; return this; - } + }; scaleAbout = (scale: number, x: number, y: number): this => { this._translateX += x * this._scale - x * this._scale * scale; this._translateY += y * this._scale - y * this._scale * scale; this._scale *= scale; return this; - } + }; transform = (transform: Transform): this => { this._translateX = transform._translateX + transform._scale * this._translateX; this._translateY = transform._translateY + transform._scale * this._translateY; this._scale *= transform._scale; return this; - } + }; preTranslate = (x: number, y: number): this => { this._translateX += this._scale * x; this._translateY += this._scale * y; return this; - } + }; preScale = (scale: number): this => { this._scale *= scale; return this; - } + }; preTransform = (transform: Transform): this => { this._translateX += transform._translateX * this._scale; this._translateY += transform._translateY * this._scale; this._scale *= transform._scale; return this; - } + }; translated = (x: number, y: number): Transform => this.copy().translate(x, y); @@ -82,18 +122,17 @@ export class Transform { y *= this._scale; y += this._translateY; return [x, y]; - } + }; transformDirection = (x: number, y: number): [number, number] => [x * this._scale, y * this._scale]; - transformBounds(x: number, y: number, width: number, height: number): { x: number, y: number, width: number, height: number } { + transformBounds(x: number, y: number, width: number, height: number): { x: number; y: number; width: number; height: number } { [x, y] = this.transformPoint(x, y); [width, height] = this.transformDirection(width, height); return { x, y, width, height }; } - inverse = () => new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale); - - copy = () => new Transform(this._translateX, this._translateY, this._scale); + inverse = () => new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale, -this._rotate); -}
\ No newline at end of file + copy = () => new Transform(this._translateX, this._translateY, this._scale, this._rotate); +} diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js index 179557953..66f8db5a1 100644 --- a/src/client/util/convertToCSSPTValue.js +++ b/src/client/util/convertToCSSPTValue.js @@ -1,18 +1,16 @@ 'use strict'; -Object.defineProperty(exports, "__esModule", { - value: true +Object.defineProperty(exports, '__esModule', { + value: true, }); exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined; exports.default = convertToCSSPTValue; exports.toClosestFontPtSize = toClosestFontPtSize; -// var _FontSizeCommandMenuButton = require('./ui/FontSizeCommandMenuButton'); - var SIZE_PATTERN = /([\d\.]+)(px|pt)/i; -var PX_TO_PT_RATIO = exports.PX_TO_PT_RATIO = 0.7518796992481203; // 1 / 1.33. -var PT_TO_PX_RATIO = exports.PT_TO_PX_RATIO = 1.33; +var PX_TO_PT_RATIO = (exports.PX_TO_PT_RATIO = 0.7518796992481203); // 1 / 1.33. +var PT_TO_PX_RATIO = (exports.PT_TO_PX_RATIO = 1.33); function convertToCSSPTValue(styleValue) { var matches = styleValue.match(SIZE_PATTERN); @@ -40,4 +38,4 @@ function toClosestFontPtSize(styleValue) { return _FontSizeCommandMenuButton.FONT_PT_SIZES.reduce(function (prev, curr) { return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev; }, Number.NEGATIVE_INFINITY); -}
\ No newline at end of file +} diff --git a/src/client/util/jsx-decl.d.ts b/src/client/util/jsx-decl.d.ts deleted file mode 100644 index 532f06178..000000000 --- a/src/client/util/jsx-decl.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-jsx-parser'; diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss index cd6a1d934..d82d7fdeb 100644 --- a/src/client/util/reportManager/ReportManager.scss +++ b/src/client/util/reportManager/ReportManager.scss @@ -1,4 +1,4 @@ -@import '../../views/global/globalCssVariables'; +@import '../../views/global/globalCssVariables.module'; // header @@ -360,5 +360,8 @@ padding: 4px 10px; font-size: 10px; border-radius: 32px; - transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; + transition: + background-color 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; } diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index b25d51b41..0c49aeed4 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,13 +1,11 @@ import * as React from 'react'; -import v4 = require('uuid/v4'); +import * as uuid from 'uuid'; import '.././SettingsManager.scss'; import './ReportManager.scss'; -import Dropzone from 'react-dropzone'; import ReactLoading from 'react-loading'; -import { action, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs'; import { CgClose } from 'react-icons/cg'; -import { AiOutlineUpload } from 'react-icons/ai'; import { HiOutlineArrowLeft } from 'react-icons/hi'; import { Issue } from './reportManagerSchema'; import { observer } from 'mobx-react'; @@ -105,6 +103,7 @@ export class ReportManager extends React.Component<{}> { constructor(props: {}) { super(props); + makeObservable(this); ReportManager.Instance = this; // initializing Github connection @@ -156,7 +155,7 @@ export class ReportManager extends React.Component<{}> { * @param files uploaded files */ private onDrop = (files: File[]) => { - this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: v4(), file }))] }); + this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: uuid.v4(), file }))] }); }; /** @@ -338,7 +337,7 @@ export class ReportManager extends React.Component<{}> { multiple onChange={e => { if (!e.target.files) return; - this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...Array.from(e.target.files).map(file => ({ _id: v4(), file }))] }); + this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...Array.from(e.target.files).map(file => ({ _id: uuid.v4(), file }))] }); }} /> {this.formData.mediaFiles.length > 0 && <ul className="file-list">{this.formData.mediaFiles.map(file => this.getMediaPreview(file))}</ul>} diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx index e870c073d..1e226bf6d 100644 --- a/src/client/util/reportManager/ReportManagerComponents.tsx +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -289,7 +289,7 @@ export const IssueView = ({ issue }: IssueViewProps) => { </div> </div> )} - <ReactMarkdown children={issueBody} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> + <ReactMarkdown children={issueBody} className="issue-content" remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> </div> ); }; |
