diff options
author | bobzel <zzzman@gmail.com> | 2024-04-21 19:03:49 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-04-21 19:03:49 -0400 |
commit | 939e18624af4252551f38c43335ee8ef0acd144c (patch) | |
tree | d4e7a8dd4db05737ec1343ff8d80611537bde65b | |
parent | 57d9c12d6b88d6814e468aca93b9bf809eabd9ce (diff) |
more lint cleanup
104 files changed, 3726 insertions, 3120 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index 0c4e375a9..780626412 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,7 @@ "react/destructuring-assignment": 0, "no-restricted-globals": ["error", "event"], "no-param-reassign": ["error", { "props": false }], + "import/no-cycle": 0, "no-alert": 0, "radix": "off" }, diff --git a/package-lock.json b/package-lock.json index cc1a0ea64..c616bb6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,6 +183,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", @@ -287,7 +288,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -30110,7 +30110,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz", "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==", - "dev": true, "peerDependencies": { "prop-types": "^15.5.4", "react": ">= 15.0.0", diff --git a/package.json b/package.json index 3f2f5a70f..a3ba6bdd9 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -267,6 +266,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 757031fec..07a2708ec 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-use-before-define */ +import Photos = require('googlephotos'); import { AssertionError } from 'assert'; import { EditorState } from 'prosemirror-state'; import { ClientUtils } from '../../../ClientUtils'; @@ -5,14 +7,12 @@ import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { RichTextUtils } from '../../../fields/RichTextUtils'; -import { Cast, StrCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; +import { Cast, ImageCast, StrCast } from '../../../fields/Types'; import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes'; import { Networking } from '../../Network'; import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents'; import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox'; import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager'; -import Photos = require('googlephotos'); export namespace GooglePhotos { const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); @@ -76,17 +76,16 @@ export namespace GooglePhotos { export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => { const { collection, title, descriptionKey, tag } = options; const dataDocument = Doc.GetProto(collection); - const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data)); if (!images || !images.length) { return undefined; } - const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`; + const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`; const { id, productUrl } = await Create.Album(resolved); const response = await Transactions.UploadImages(images, { id }, descriptionKey); if (response) { const { results, failed } = response; - let index: Opt<number>; - while ((index = failed.pop()) !== undefined) { + for (let index = failed.pop(); index !== undefined; index = failed.pop()) { Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]); } const mediaItems: MediaItem[] = results.map(item => item.mediaItem); @@ -97,13 +96,12 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; - if (!mediaItem) { - continue; + if (mediaItem) { + image.googlePhotosId = mediaItem.id; + image.googlePhotosAlbumUrl = productUrl; + image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; + idMapping[mediaItem.id] = image; } - image.googlePhotosId = mediaItem.id; - image.googlePhotosAlbumUrl = productUrl; - image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; - idMapping[mediaItem.id] = image; } collection.googlePhotosAlbumUrl = productUrl; collection.googlePhotosIdMapping = idMapping; @@ -114,6 +112,7 @@ export namespace GooglePhotos { Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); return { albumId: id, mediaItems }; } + return undefined; }; } @@ -124,7 +123,7 @@ export namespace GooglePhotos { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const response = await Query.ContentSearch(requested); const uploads = await Transactions.WriteMediaItemsToServer(response); - const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/)); + const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */)); const options = { _width: 500, _height: 500 }; return constructor(children, options); }; @@ -144,7 +143,7 @@ export namespace GooglePhotos { const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); - for (const value of values) { + values.forEach(async value => { const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id); searched?.forEach(async id => { const image = await Cast(idMapping[id], Doc); @@ -154,7 +153,7 @@ export namespace GooglePhotos { !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); } }); - } + }); images?.forEach(image => { const concatenated = tagMapping.get(image[Id])!; const tags = concatenated.split(delimiter); @@ -200,9 +199,10 @@ export namespace GooglePhotos { export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => { const photos = await endpoint(); const mediaItems: MediaItem[] = []; - let nextPageTokenStored: Opt<string> = undefined; + let nextPageTokenStored: Opt<string>; const found = 0; do { + // eslint-disable-next-line no-await-in-loop const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored); mediaItems.push(...response.mediaItems); nextPageTokenStored = response.nextPageToken; @@ -222,7 +222,7 @@ export namespace GooglePhotos { excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); filters.setContentFilter(contentFilter); - const date = options.date; + const { date } = options; if (date) { const dateFilter = new photos.DateFilter(); if (date instanceof Date) { @@ -240,15 +240,11 @@ export namespace GooglePhotos { }); }; - export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => { - return (await endpoint()).mediaItems.get(mediaItemId); - }; + export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => (await endpoint()).mediaItems.get(mediaItemId); } namespace Create { - export const Album = async (title: string) => { - return (await endpoint()).albums.create(title); - }; + export const Album = async (title: string) => (await endpoint()).albums.create(title); } export namespace Transactions { @@ -278,6 +274,7 @@ export namespace GooglePhotos { return enrichmentItem.id; } } + return undefined; }; export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => { @@ -291,9 +288,12 @@ export namespace GooglePhotos { return undefined; } const baseUrls: string[] = await Promise.all( - response.results.map(item => { - return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); - }) + response.results.map( + item => + new Promise<string>(resolve => { + Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)); + }) + ) ); return baseUrls; }; @@ -303,27 +303,25 @@ export namespace GooglePhotos { failed: number[]; } - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => { + export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - if (album && 'title' in album) { - album = await Create.Album(album.title); - } + const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn; const media: MediaInput[] = []; - for (const source of sources) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; - } - const url = data.url.href; - const target = Doc.MakeEmbedding(source); - const description = parseDescription(target, descriptionKey); - await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); - media.push({ url, description }); - } + sources + .filter(source => ImageCast(Doc.GetProto(source).data)) + .forEach(async source => { + const data = ImageCast(Doc.GetProto(source).data); + const url = data.url.href; + const target = Doc.MakeEmbedding(source); + const description = parseDescription(target, descriptionKey); + await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); + media.push({ url, description }); + }); if (media.length) { const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album }); return results; } + return undefined; }; const parseDescription = (document: Doc, descriptionKey: string) => { diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx index 02879e3c4..28c00644f 100644 --- a/src/client/util/BranchingTrailManager.tsx +++ b/src/client/util/BranchingTrailManager.tsx @@ -1,18 +1,31 @@ +/* eslint-disable react/no-unused-class-component-methods */ +/* eslint-disable react/no-array-index-key */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { PresBox } from '../views/nodes/trails'; import { OverlayView } from '../views/OverlayView'; +import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; -import { Docs } from '../documents/Documents'; -import { nullAudio } from '../../fields/URLField'; @observer export class BranchingTrailManager extends React.Component { + // eslint-disable-next-line no-use-before-define public static Instance: BranchingTrailManager; + // stack of the history + @observable private slideHistoryStack: String[] = []; + @observable private containsSet: Set<String> = new Set<String>(); + // docId to Doc map + @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>(); + + // prev pres to copmare with + @observable private prevPresId: String | null = null; + @action setPrevPres = action((newId: String | null) => { + this.prevPresId = newId; + }); + constructor(props: any) { super(props); makeObservable(this); @@ -22,7 +35,7 @@ export class BranchingTrailManager extends React.Component { } setupUi = () => { - OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); + OverlayView.Instance.addWindow(<BranchingTrailManager />, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); // OverlayView.Instance.forceUpdate(); console.log(OverlayView.Instance); // let hi = Docs.Create.TextDocument("beee", { @@ -36,23 +49,11 @@ export class BranchingTrailManager extends React.Component { console.log(DocumentManager._overlayViews); }; - // stack of the history - @observable private slideHistoryStack: String[] = []; @action setSlideHistoryStack = action((newArr: String[]) => { this.slideHistoryStack = newArr; }); - @observable private containsSet: Set<String> = new Set<String>(); - - // prev pres to copmare with - @observable private prevPresId: String | null = null; - @action setPrevPres = action((newId: String | null) => { - this.prevPresId = newId; - }); - - // docId to Doc map - @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>(); - + // eslint-disable-next-line react/sort-comp observeDocumentChange = (targetDoc: Doc, pres: PresBox) => { const presId = pres.Document[Id]; if (this.prevPresId === presId) { @@ -106,7 +107,7 @@ export class BranchingTrailManager extends React.Component { if (this.slideHistoryStack.length === 0) { Doc.UserDoc().isBranchingMode = false; } - //PresBox.NavigateToTarget(targetDoc, targetDoc); + // PresBox.NavigateToTarget(targetDoc, targetDoc); }; @computed get trailBreadcrumbs() { @@ -116,11 +117,11 @@ export class BranchingTrailManager extends React.Component { const [presId, targetDocId] = info.split(','); const doc = this.docIdToDocMap.get(targetDocId); if (!doc) { - return <></>; + return null; } return ( <span key={targetDocId}> - <button key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}> + <button type="button" key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}> {presId.slice(0, 3) + ':' + doc.title} </button> -{'>'} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 6e9094b3a..46aa4d238 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -1,4 +1,10 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; +import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { TextField } from '@mui/material'; +import { Button } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -6,21 +12,16 @@ import Select from 'react-select'; import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { StrCast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './CalendarManager.scss'; import { DocumentManager } from './DocumentManager'; import { SelectionManager } from './SelectionManager'; import { SettingsManager } from './SettingsManager'; -// import { DateRange, Range, RangeKeyDict } from 'react-date-range'; -import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; -import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button } from 'browndash-components'; -import { Docs } from '../documents/Documents'; -import { ObservableReactComponent } from '../views/ObservableReactComponent'; // import 'react-date-range/dist/styles.css'; // import 'react-date-range/dist/theme/default.css'; @@ -47,6 +48,7 @@ const formatCalendarDateToString = (calendarDate: any) => { @observer export class CalendarManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: CalendarManager; @observable private isOpen = false; @observable private targetDoc: Doc | undefined = undefined; // the target document @@ -83,10 +85,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { this.creationType = type; }; - public open = (target?: DocumentView, target_doc?: Doc) => { + public open = (target?: DocumentView, targetDoc?: Doc) => { console.log('hi'); runInAction(() => { - this.targetDoc = target_doc || target?.Document; + this.targetDoc = targetDoc || target?.Document; this.targetDocView = target; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; @@ -117,7 +119,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action handleSelectChange = (option: any) => { if (option) { - let selectOpt = option as CalendarSelectOptions; + const selectOpt = option as CalendarSelectOptions; this.selectedExistingCalendarOption = selectOpt; this.calendarName = selectOpt.value; // or label } @@ -136,7 +138,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { // TODO: Make undoable private addToCalendar = () => { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar console.log(targetDoc); @@ -159,7 +161,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { } } else { // find existing calendar based on selected name (should technically always find one) - const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName); + const existingCalendar = this.existingCalendars.find(findCal => StrCast(findCal.title) === this.calendarName); if (existingCalendar) calendar = existingCalendar; else { this.errorMessage = 'Must select an existing calendar'; @@ -252,11 +254,9 @@ export class CalendarManager extends ObservableReactComponent<{}> { @computed get calendarInterface() { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - const currentDate = new Date(); - return ( <div className="calendar-interface" @@ -268,10 +268,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { <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')}> + <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => 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')}> + <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => this.setInterationType('existing-calendar')}> Add to Existing calendar </div> </div> @@ -317,7 +317,8 @@ export class CalendarManager extends ObservableReactComponent<{}> { color: StrCast(Doc.UserDoc().userColor), width: '100%', }), - }}></Select> + }} + /> )} </div> <div className="description-container"> @@ -351,6 +352,6 @@ export class CalendarManager extends ObservableReactComponent<{}> { } render() { - return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 27ae5c9a0..6dba8027d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -709,7 +709,7 @@ pie title Minerals in my tap water return [ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(); }' }}, { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(); }'}}, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, ]; } static videoTools() { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 62f055f1a..3890b7845 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -495,18 +495,20 @@ export namespace DragManager { .filter(pb => pb.width && pb.height) .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } - [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => { - (ele as any).style && ((ele as any).style.pointerEvents = 'none'); - }); + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))] + .map(dele => (dele as any).style) + .forEach(style => { + style && (style.pointerEvents = 'none'); + }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { - const children = [Array.from(ele.children), Array.from(dragElement.children)]; - while (children[0].length) { - const childs = [children[0].pop(), children[1].pop()]; + const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)]; + while (dragChildren[0].length) { + const childs = [dragChildren[0].pop(), dragChildren[1].pop()]; if (childs[0]?.children) { - children[0].push(...Array.from(childs[0].children)); - children[1].push(...Array.from(childs[1]!.children)); + dragChildren[0].push(...Array.from(childs[0].children)); + dragChildren[1].push(...Array.from(childs[1]!.children)); } if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index c261c0f1e..8d84dbad8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -30,6 +32,7 @@ export interface UserOptions { @observer export class GroupManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @observable private users: string[] = []; // list of users populated from the database. @@ -160,7 +163,7 @@ export class GroupManager extends ObservableReactComponent<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); - this.GroupManagerDoc['data_modificationDate'] = new DateField(); + this.GroupManagerDoc.data_modificationDate = new DateField(); return true; } return false; @@ -202,7 +205,7 @@ export class GroupManager extends ObservableReactComponent<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } @@ -216,10 +219,9 @@ export class GroupManager extends ObservableReactComponent<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { - const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } } @@ -275,7 +277,9 @@ export class GroupManager extends ObservableReactComponent<{}> { TaskCompletionBox.textDisplayed = 'Group created!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); }; @@ -292,7 +296,7 @@ export class GroupManager extends ObservableReactComponent<{}> { </p> <div className="close-button"> <Button - icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} + icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={action(() => { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; @@ -302,7 +306,17 @@ export class GroupManager extends ObservableReactComponent<{}> { </div> </div> <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}> - <input ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} /> + <input + ref={this.inputRef} + onKeyDown={this.handleKeyDown} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus + type="text" + placeholder="Group name" + onChange={action(() => { + this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'; + })} + /> </div> <div style={{ border: StrCast(Doc.UserDoc().userColor) }}> <Select @@ -310,7 +324,7 @@ export class GroupManager extends ObservableReactComponent<{}> { isMulti options={this.options} onChange={this.handleChange} - placeholder={'Select users'} + placeholder="Select users" value={this.selectedUsers} closeMenuOnSelect={false} styles={{ @@ -335,8 +349,8 @@ export class GroupManager extends ObservableReactComponent<{}> { }} /> </div> - <div className={'create-button'}> - <Button text={'Create'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} /> + <div className="create-button"> + <Button text="Create" type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} /> </div> </div> ); @@ -344,7 +358,7 @@ export class GroupManager extends ObservableReactComponent<{}> { return ( <MainViewModal isDisplayed={this.createGroupModalOpen} - interactive={true} + interactive contents={contents} dialogueBoxStyle={{ width: '90%', height: '70%' }} closeOnExternalClick={action(() => { @@ -372,28 +386,59 @@ export class GroupManager extends ObservableReactComponent<{}> { return ( <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> {this.groupCreationModal} - {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null} + {this.currentGroup ? ( + <GroupMemberView + group={this.currentGroup} + onCloseButtonClick={action(() => { + this.currentGroup = undefined; + })} + /> + ) : null} <div className="group-heading"> <p> <b>Manage Groups</b> </p> - <Button icon={<FontAwesomeIcon icon={'plus'} />} iconPlacement={'left'} text={'Create Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} /> - <div className={'close-button'}> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} /> + <Button + icon={<FontAwesomeIcon icon="plus" />} + iconPlacement="left" + text="Create Group" + type={Type.TERT} + color={StrCast(Doc.UserDoc().userColor)} + onClick={action(() => { + this.createGroupModalOpen = true; + })} + /> + <div className="close-button"> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} /> </div> </div> <div className="main-container"> - <div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + <div + className="sort-groups" + onClick={action(() => { + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> Name <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} /> </div> - <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} /> + <div className="style-divider" style={{ background: StrCast(Doc.UserDoc().userColor) }} /> <div className="group-body" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}> {groups.map(group => ( <div className="group-row" key={StrCast(group.title || group.groupName)}> <div className="group-name">{StrCast(group.title || group.groupName)}</div> - <div className="group-info" onClick={action(() => (this.currentGroup = group))}> - <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.currentGroup = group))} /> + <div + className="group-info" + onClick={action(() => { + this.currentGroup = group; + })}> + <IconButton + icon={<FontAwesomeIcon icon="info-circle" />} + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + onClick={action(() => { + this.currentGroup = group; + })} + /> </div> </div> ))} @@ -404,6 +449,6 @@ export class GroupManager extends ObservableReactComponent<{}> { } render() { - return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 48d3ac046..6dc96f1b5 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -3,12 +3,10 @@ import { simulateMouseClick } from '../../ClientUtils'; 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 { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; import { DocumentView } from '../views/nodes/DocumentView'; import { DocumentManager } from './DocumentManager'; -import { SearchUtil } from './SearchUtil'; import { SelectionManager } from './SelectionManager'; export namespace Hypothesis { diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index dfad9755c..8d4eefa7e 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -16,7 +16,7 @@ export namespace ImageUtils { }; export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => { const field = Cast(document.data, ImageField); - return field ? await Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; + return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; }; export const AssignImgInfo = (document: Doc, data?: imgInfo) => { diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index ba715aa1f..85bada8c9 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -53,7 +53,7 @@ export class LinkFollower { const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks; const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); - var count = 0; + let count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); if (!followLinks.length) { finished?.(); @@ -69,7 +69,7 @@ export class LinkFollower { ? linkDoc.link_anchor_2 : linkDoc.link_anchor_1 ) as Doc; - const srcAnchor = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc; + const srcAnchor: Doc = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc; if (target) { const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); @@ -113,10 +113,12 @@ export class LinkFollower { } const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) { + // eslint-disable-next-line prefer-destructuring target.x = moveTo[0]; movedTarget = true; } if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) { + // eslint-disable-next-line prefer-destructuring target.y = moveTo[1]; movedTarget = true; } @@ -130,6 +132,7 @@ export class LinkFollower { } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) { SelectionManager.DeselectAll(); return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true }; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 82cd791cc..c986bd674 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -25,6 +25,7 @@ import { ScriptingGlobals } from './ScriptingGlobals'; * - user defined kvps */ export class LinkManager { + // eslint-disable-next-line no-use-before-define @observable static _instance: LinkManager; @observable.shallow userLinkDBs: Doc[] = []; @observable public currentLink: Opt<Doc> = undefined; @@ -61,7 +62,7 @@ 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( link && - action(lAnchProtoProtos => { + action(() => { Doc.AddDocToList(Doc.UserDoc(), 'links', link); lAnchs[0]?.[DocData][DirectLinks].add(link); lAnchs[1]?.[DocData][DirectLinks].add(link); @@ -78,7 +79,7 @@ export class LinkManager { Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) => 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 => { + action(() => { link && lAnchs[0] && lAnchs[0][DocData][DirectLinks].delete(link); link && lAnchs[1] && lAnchs[1][DocData][DirectLinks].delete(link); }) @@ -100,7 +101,8 @@ export class LinkManager { (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); break; - case 'update': //let oldValue = change.oldValue; + case 'update': // let oldValue = change.oldValue; + default: } }, true @@ -120,6 +122,8 @@ export class LinkManager { added?.forEach((link: any) => addLinkToDoc(toRealField(link))); removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); }); + break; + default: } }, true @@ -133,7 +137,8 @@ export class LinkManager { case 'splice': (change as any).added.forEach(watchUserLinkDB); break; - case 'update': //let oldValue = change.oldValue; + case 'update': // let oldValue = change.oldValue; + default: } }, true @@ -143,7 +148,7 @@ export class LinkManager { } public createlink_relationshipLists = () => { - //create new lists for link relations and their associated colors if the lists don't already exist + // create new lists for link relations and their associated colors if the lists don't already exist !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>()); !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>()); !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>()); @@ -153,6 +158,7 @@ export class LinkManager { Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); + // eslint-disable-next-line no-use-before-define setTimeout(UPDATE_SERVER_CACHE, 100); } } @@ -203,7 +209,7 @@ export class LinkManager { this.relatedLinker(anchor).forEach(link => { if (link.link_relationship && link.link_relationship !== '-ungrouped-') { const relation = StrCast(link.link_relationship); - const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; + const anchorRelation: string = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; const group = anchorGroups.get(anchorRelation); anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { @@ -234,14 +240,7 @@ export class LinkManager { 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'; + return undefined; } } @@ -282,6 +281,7 @@ export function UPDATE_SERVER_CACHE() { } ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function links(doc: any) { return new List(LinkManager.Links(doc)); }, diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts index bc159ed65..ac524394a 100644 --- a/src/client/util/ScriptingGlobals.ts +++ b/src/client/util/ScriptingGlobals.ts @@ -41,6 +41,7 @@ export namespace ScriptingGlobals { if (n === undefined || n === 'undefined') { return false; } + // eslint-disable-next-line no-prototype-builtins if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 65b9a977d..609fedfa9 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -8,16 +8,16 @@ import { DocOptions, FInfo } from '../documents/Documents'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; - export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean, onlyKeys?: string[]) { + export function SearchCollection(collectionDoc: Opt<Doc>, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) { const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = matchKeyNames ? [] : Object.entries(DocOptions) - .filter(([key, info]: [string, FInfo]) => !info?.searchable()) + .filter(([, info]: [string, FInfo]) => !info?.searchable()) .map(([key]) => key); - const exact = query.startsWith('='); - query = query.toLowerCase().split('=').lastElement(); + const exact = queryIn.startsWith('='); + const query = queryIn.toLowerCase().split('=').lastElement(); const results = new ObservableMap<Doc, string[]>(); if (collectionDoc) { @@ -51,7 +51,11 @@ export namespace SearchUtil { */ export function documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; - Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false))); + Doc.GetAllPrototypes(doc).map(proto => + Object.keys(proto).forEach(key => { + keys[key] = false; + }) + ); return Array.from(Object.keys(keys)); } @@ -62,12 +66,14 @@ export namespace SearchUtil { * This method iterates through an array of docs and all docs within those docs, calling * the function func on each doc. */ - export function foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { + export function foreachRecursiveDoc(docsIn: Doc[], func: (depth: number, doc: Doc) => void) { + let docs = docsIn; let newarray: Doc[] = []; - var depth = 0; + let depth = 0; const visited: Doc[] = []; while (docs.length > 0) { newarray = []; + // eslint-disable-next-line no-loop-func docs.filter(d => d && !visited.includes(d)).forEach(d => { visited.push(d); const fieldKey = Doc.LayoutFieldKey(d); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index f983c29b7..8347844f7 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; @@ -17,8 +19,8 @@ import { MainViewModal } from '../views/MainViewModal'; import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; -import { undoBatch } from './UndoManager'; import { SnappingManager } from './SnappingManager'; +import { undoable } from './UndoManager'; export enum ColorScheme { Dark = 'Dark', @@ -38,24 +40,104 @@ export class SettingsManager extends React.Component<{}> { // eslint-disable-next-line no-use-before-define public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); - @observable public isOpen = false; - @observable private passwordResultText = ''; - @observable private playgroundMode = false; + @observable private _passwordResultText = ''; + @observable private _playgroundMode = false; - @observable private curr_password = ''; - @observable private new_password = ''; - @observable private new_confirm = ''; + @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 private _activeTab = 'Accounts'; + @observable private _isOpen = false; @observable public propertiesWidth: number = 0; + private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); + + public closeMgr = action(() => { + this._isOpen = false; + }); + public openMgr = action(() => { + this._isOpen = true; + }); + + private matchSystem = undoable(() => { + if (Doc.UserDoc().userThemeSystem) { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); + if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); + } + }, 'match system theme'); + private setFreeformScrollMode = undoable((mode: string) => { + Doc.UserDoc().freeformScrollMode = mode; + }, 'set scroll mode'); + private selectUserMode = undoable((mode: string) => { + Doc.noviceMode = mode === 'Novice'; + }, 'change user mode'); + private changeFontFamily = undoable((font: string) => { + Doc.UserDoc().fontFamily = font; + }, 'change font family'); + private switchUserBackgroundColor = undoable((color: string) => { + Doc.UserDoc().userBackgroundColor = color; + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); + }, 'change background color'); + private switchUserColor = undoable((color: string) => { + Doc.UserDoc().userColor = color; + }, 'change user color'); + switchUserVariantColor = undoable((color: string) => { + Doc.UserDoc().userVariantColor = color; + }, 'change variant color'); + userThemeSystemToggle = undoable(() => { + Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem; + this.matchSystem(); + }, 'change theme color'); + playgroundModeToggle = undoable( + action(() => { + this._playgroundMode = !this._playgroundMode; + if (this._playgroundMode) { + DocServer.Control.makeReadOnly(); + addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); + } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); + }), + 'set playgorund mode' + ); + changeColorScheme = undoable( + action((scheme: string) => { + Doc.UserDoc().userTheme = scheme; + switch (scheme) { + case ColorScheme.Light: + this.switchUserColor('#323232'); + this.switchUserBackgroundColor('#DFDFDF'); + this.switchUserVariantColor('#BDDDF5'); + break; + case ColorScheme.Dark: + this.switchUserColor('#DFDFDF'); + this.switchUserBackgroundColor('#323232'); + this.switchUserVariantColor('#4476F7'); + break; + case ColorScheme.CoolBlue: + this.switchUserColor('#ADEAFF'); + this.switchUserBackgroundColor('#060A15'); + this.switchUserVariantColor('#3C51FF'); + break; + case ColorScheme.Cupcake: + this.switchUserColor('#3BC7FF'); + this.switchUserBackgroundColor('#fffdf7'); + this.switchUserVariantColor('#FFD7F3'); + break; + case ColorScheme.Custom: + break; + default: + } + }), + 'change color scheme' + ); + constructor(props: {}) { super(props); makeObservable(this); SettingsManager.Instance = this; this.matchSystem(); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (Doc.UserDoc().userThemeSystem) { if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); @@ -73,26 +155,9 @@ export class SettingsManager extends React.Component<{}> { ); SnappingManager.SettingsStyle = SettingsManager._settingsStyle; } - matchSystem = () => { - if (Doc.UserDoc().userThemeSystem) { - if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); - if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); - } - }; - - public close = action(() => (this.isOpen = false)); - public open = action(() => (this.isOpen = true)); - private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); - private changePassword = async () => { - if (!(this.curr_password && this.new_password && this.new_confirm)) { - runInAction(() => (this.passwordResultText = "Error: Hey, we're missing some fields!")); - } else { - const passwordBundle = { curr_pass: this.curr_password, new_pass: this.new_password, new_confirm: this.new_confirm }; - const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle); - runInAction(() => (this.passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!')); - } - }; + public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore + public set LastPressedBtn(state:Doc|undefined) { this._lastPressedSidebarBtn = state; } // prettier-ignore @computed public static get userColor() { return StrCast(Doc.UserDoc().userColor); @@ -106,60 +171,6 @@ 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)); - @undoBatch changeFontSize = action((val: number) => (Doc.UserDoc().fontSize = val)); - @undoBatch switchUserBackgroundColor = action((color: string) => { - Doc.UserDoc().userBackgroundColor = color; - addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); - }); - @undoBatch switchUserColor = action((color: string) => (Doc.UserDoc().userColor = color)); - @undoBatch switchUserVariantColor = action((color: string) => (Doc.UserDoc().userVariantColor = color)); - @undoBatch userThemeSystemToggle = action(() => { - Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem; - this.matchSystem(); - }); - @undoBatch playgroundModeToggle = action(() => { - this.playgroundMode = !this.playgroundMode; - if (this.playgroundMode) { - DocServer.Control.makeReadOnly(); - addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); - }); - - @undoBatch - changeColorScheme = action((scheme: string) => { - Doc.UserDoc().userTheme = scheme; - switch (scheme) { - case ColorScheme.Light: - this.switchUserColor('#323232'); - this.switchUserBackgroundColor('#DFDFDF'); - this.switchUserVariantColor('#BDDDF5'); - break; - case ColorScheme.Dark: - this.switchUserColor('#DFDFDF'); - this.switchUserBackgroundColor('#323232'); - this.switchUserVariantColor('#4476F7'); - break; - case ColorScheme.CoolBlue: - this.switchUserColor('#ADEAFF'); - this.switchUserBackgroundColor('#060A15'); - this.switchUserVariantColor('#3C51FF'); - break; - case ColorScheme.Cupcake: - this.switchUserColor('#3BC7FF'); - this.switchUserBackgroundColor('#fffdf7'); - this.switchUserVariantColor('#FFD7F3'); - break; - case ColorScheme.Custom: - break; - } - }); - @computed get colorsContent() { const schemeMap = Array.from(Object.keys(ColorScheme)); const userTheme = StrCast(Doc.UserDoc().userTheme); @@ -227,7 +238,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show document header" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} + onClick={() => { + Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date'; + }} toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} size={Size.XSMALL} color={SettingsManager.userColor} @@ -236,8 +249,10 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Full Toolbar" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} - toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} + onClick={() => { + Doc.UserDoc().documentLinksButton_fullMenu = !Doc.UserDoc().documentLinksButton_fullMenu; + }} + toggleStatus={BoolCast(Doc.UserDoc().documentLinksButton_fullMenu)} size={Size.XSMALL} color={SettingsManager.userColor} /> @@ -245,7 +260,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Button Labels" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)} + onClick={() => { + FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels; + }} toggleStatus={FontIconBox.ShowIconLabels} size={Size.XSMALL} color={SettingsManager.userColor} @@ -254,7 +271,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Recognize Ink Gestures" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)} + onClick={() => { + GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures; + }} toggleStatus={GestureOverlay.RecognizeGestures} size={Size.XSMALL} color={SettingsManager.userColor} @@ -263,7 +282,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Hide Labels In Ink Shapes" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} + onClick={() => { + Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels; + }} toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -272,7 +293,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Open Ink Docs in Lightbox" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} + onClick={() => { + Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox; + }} toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -281,7 +304,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Link Lines" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)} + onClick={() => { + Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines; + }} toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -296,7 +321,9 @@ export class SettingsManager extends React.Component<{}> { step={2} type={Type.TERT} unit="px" - setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))} + setNumber={val => { + Doc.UserDoc().headerHeight = val; + }} /> </Group> </div> @@ -320,7 +347,6 @@ export class SettingsManager extends React.Component<{}> { @computed get textContent() { const fontFamilies = ['Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Roboto']; - const fontSizes = ['7px', '8px', '9px', '10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '48px', '72px']; return ( <div className="tab-content appearances-content"> @@ -338,19 +364,19 @@ export class SettingsManager extends React.Component<{}> { type={Type.PRIM} number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))} unit="px" - setNumber={val => (Doc.UserDoc().fontSize = val + 'px')} + setNumber={val => { + Doc.UserDoc().fontSize = val + 'px'; + }} /> <Dropdown - items={fontFamilies.map(val => { - return { - text: val, - val: val, - style: { - fontFamily: val, - }, - }; - })} - closeOnSelect={true} + items={fontFamilies.map(val => ({ + text: val, + val: val, + style: { + fontFamily: val, + }, + }))} + closeOnSelect dropdownType={DropdownType.SELECT} type={Type.TERT} selectedVal={StrCast(Doc.UserDoc().fontFamily)} @@ -367,28 +393,13 @@ export class SettingsManager extends React.Component<{}> { ); } - @action - changeVal = (value: string, pass: string) => { - switch (pass) { - case 'curr': - this.curr_password = value; - break; - case 'new': - this.new_password = value; - break; - case 'conf': - this.new_confirm = value; - break; - } - }; - @computed get passwordContent() { return ( <div className="password-content"> <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> - {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} + {!this._passwordResultText ? null : <div className={`${this._passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this._passwordResultText}</div>} <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} /> <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} /> </div> @@ -418,10 +429,6 @@ export class SettingsManager extends React.Component<{}> { ); } - setFreeformScrollMode = (mode: string) => { - Doc.UserDoc().freeformScrollMode = mode; - }; - @computed get modesContent() { return ( <div className="tab-content modes-content"> @@ -430,7 +437,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <Dropdown formLabel="Mode" - closeOnSelect={true} + closeOnSelect items={[ { text: 'Novice', @@ -454,7 +461,7 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} fillWidth /> - <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> + <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this._playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> </div> <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}> Freeform Navigation @@ -462,7 +469,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <Dropdown formLabel="Scroll Mode" - closeOnSelect={true} + closeOnSelect items={[ { text: 'Scroll to Pan', @@ -493,10 +500,28 @@ export class SettingsManager extends React.Component<{}> { formLabel="Default access private" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} - onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} + onClick={action(() => { + Doc.defaultAclPrivate = !Doc.defaultAclPrivate; + })} + /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel="Enable Sharing UI" + color={SettingsManager.userColor} + toggleStatus={BoolCast(Doc.IsSharingEnabled)} + onClick={action(() => { + Doc.IsSharingEnabled = !Doc.IsSharingEnabled; + })} + /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel="Disable Info UI" + color={SettingsManager.userColor} + toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} + onClick={action(() => { + Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled; + })} /> - <Toggle toggleType={ToggleType.SWITCH} formLabel="Enable Sharing UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} /> - <Toggle toggleType={ToggleType.SWITCH} formLabel="Disable Info UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} onClick={action(() => (Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled))} /> </div> </div> </div> @@ -518,7 +543,7 @@ export class SettingsManager extends React.Component<{}> { <div className="settings-panel" style={{ background: SettingsManager.userColor }}> <div className="settings-tabs"> {tabs.map(tab => { - const isActive = this.activeTab === tab.title; + const isActive = this._activeTab === tab.title; return ( <div key={tab.title} @@ -527,7 +552,9 @@ export class SettingsManager extends React.Component<{}> { color: isActive ? SettingsManager.userColor : SettingsManager.userBackgroundColor, }} className={'tab-control ' + (isActive ? 'active' : 'inactive')} - onClick={action(() => (this.activeTab = tab.title))}> + onClick={action(() => { + this._activeTab = tab.title; + })}> {tab.title} </div> ); @@ -544,12 +571,12 @@ export class SettingsManager extends React.Component<{}> { </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.closeMgr} color={SettingsManager.userColor} /> </div> <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> {tabs.map(tab => ( - <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}> + <div key={tab.title} className={'tab-section ' + (this._activeTab === tab.title ? 'active' : 'inactive')}> {tab.ele} </div> ))} @@ -558,13 +585,37 @@ export class SettingsManager extends React.Component<{}> { ); } + private changePassword = async () => { + if (!(this._curr_password && this._new_password && this._new_confirm)) { + runInAction(() => { + this._passwordResultText = "Error: Hey, we're missing some fields!"; + }); + } else { + const passwordBundle = { curr_pass: this._curr_password, new_pass: this._new_password, new_confirm: this._new_confirm }; + const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle); + runInAction(() => { + this._passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!'; + }); + } + }; + + @action + changeVal = (value: string, pass: string) => { + switch (pass) { + case 'curr': this._curr_password = value; break; + case 'new': this._new_password = value; break; + case 'conf': this._new_confirm = value; break; + default: + } // prettier-ignore + }; + render() { return ( <MainViewModal contents={this.settingsInterface} - isDisplayed={this.isOpen} - interactive={true} - closeOnExternalClick={this.close} + isDisplayed={this._isOpen} + interactive + closeOnExternalClick={this.closeMgr} dialogueBoxStyle={{ width: 'fit-content', height: '300px', background: Cast(Doc.UserDoc().userColor, 'string', null) }} /> ); diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index ade4cc218..6676e4e03 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { concat, intersection } from 'lodash'; @@ -65,7 +68,10 @@ interface ValidatedUser { @observer export class SharingManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: SharingManager; + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + private populating: boolean = false; // whether the list of users is populating or not @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 = undefined; // the document being shared @@ -77,11 +83,9 @@ export class SharingManager extends React.Component<{}> { @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) - private populating: boolean = false; // whether the list of users is populating or not @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not @@ -90,33 +94,6 @@ export class SharingManager extends React.Component<{}> { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; // } - public open = (target?: DocumentView, target_doc?: Doc) => { - this.populateUsers(); - runInAction(() => { - this.targetDocView = target; - this.targetDoc = target_doc || target?.Document; - DictationOverlay.Instance.hasActiveModal = true; - this.isOpen = this.targetDoc !== undefined; - this.permissions = SharingPermissions.Augment; - this.upgradeNested = true; - }); - }; - - public close = action(() => { - this.isOpen = false; - this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) - TaskCompletionBox.taskCompleted = false; - setTimeout( - action(() => { - // this.copied = false; - DictationOverlay.Instance.hasActiveModal = false; - this.targetDoc = undefined; - }), - 500 - ); - this.layoutDocAcls = false; - }); - constructor(props: {}) { super(props); makeObservable(this); @@ -131,226 +108,6 @@ 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] !== Utils.GuestID()) { - this.populating = true; - const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); - runInAction(() => { - FieldLoader.ServerLoadStatus.message = 'users'; - }); - const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); - raw.map( - action((newUser: User) => { - const sharingDoc = docs[newUser.sharingDocumentId]; - const linkDatabase = docs[newUser.linkDatabaseId]; - if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { - if (!this.users.find(user => user.user.email === newUser.email)) { - this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - //LinkManager.addLinkDB(linkDatabase); - } - } - }) - ); - this.populating = false; - } - }; - - /** - * Shares the document with a user. - */ - setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { - 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.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) { - Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); - } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); - }); - }, 'set Doc permissions'); - - /** - * Sets the permission on the target for the group. - * @param group - * @param permission - */ - setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(StrCast(group.title))}`; - - 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 (group instanceof Doc) { - Doc.AddDocToList(group, 'docsShared', doc); - - this.users - .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) - .forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) - Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists - }); - } - }); - }, 'set group permissions'); - - /** - * Shares the documents shared with a group with a new user who has been added to that group. - * @param group - * @param emailId - */ - shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { - const user = this.users.find(({ user: { email } }) => email === emailId)!; - const self = this; - if (group.docsShared) { - if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); - else { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - } - } - }; - - /** - * Called from the properties sidebar to change permissions of a user. - */ - shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { - if (layout) this.layoutDocAcls = true; - if (shareWith !== 'Guest') { - const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); - docs.forEach(doc => { - if (user) this.setInternalSharing(user, permission, doc); - else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); - }); - } else { - docs.forEach(doc => { - if (GetEffectiveAcl(doc) === AclAdmin) { - distributeAcls(`acl-${shareWith}`, permission, doc, undefined); - } - }); - } - this.layoutDocAcls = false; - }, 'sidebar set permissions'); - - /** - * Removes the documents shared with a user through a group when the user is removed from the group. - * @param group - * @param emailId - */ - removeMember = (group: Doc, emailId: string) => { - const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - - if (group.docsShared && user) { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - } - }; - - /** - * Removes a group's permissions from documents that have been shared with it. - * @param group - */ - removeGroup = (group: Doc) => { - if (group.docsShared) { - DocListCast(group.docsShared).forEach(doc => { - const acl = `acl-${StrCast(group.title)}`; - distributeAcls(acl, SharingPermissions.None, doc); - - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - - users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); - }); - } - }; - - // private setExternalSharing = (permission: string) => { - // const targetDoc = this.targetDoc; - // if (!targetDoc) { - // return; - // } - // targetDoc["acl-" + PublicKey] = permission; - // }s - - /** - * Copies the Public sharing url to the user's clipboard. - */ - private copyURL = (e: any) => { - ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); - }; - - /** - * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share - */ - private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { - const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift('-multiple-'); - return dropdownValues.map(permission => ( - <option key={permission} value={permission}> - {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} - </option> - )); - } - - 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]; - 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> - ); - }; - - /** * Handles changes in the users selected in react-select */ @action @@ -369,57 +126,6 @@ export class SharingManager extends React.Component<{}> { ); /** - * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. - */ - share = undoable( - action(() => { - if (this.selectedUsers) { - this.selectedUsers.forEach(user => { - if (user.value.includes(indType)) { - this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); - } else { - this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); - } - }); - - if (this.shareDocumentButtonRef.current) { - const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); - TaskCompletionBox.popupX = left - 1.5 * width; - TaskCompletionBox.popupY = top - 1.5 * height; - TaskCompletionBox.textDisplayed = 'Document shared!'; - TaskCompletionBox.taskCompleted = true; - setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), - 2000 - ); - } - - this.layoutDocAcls = false; - this.selectedUsers = null; - } - }), - 'share Doc' - ); - - /** - * Sorting algorithm to sort users. - */ - sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { - const { email: e1 } = u1.user; - const { email: e2 } = u2.user; - return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; - }; - - /** - * Sorting algorithm to sort groups. - */ - sortGroups = (group1: Doc, group2: Doc) => { - const g1 = StrCast(group1.title); - const g2 = StrCast(group2.title); - return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; - }; - - /** * @returns the main interface of the SharingManager. */ @computed get sharingInterface() { @@ -477,8 +183,8 @@ export class SharingManager extends React.Component<{}> { permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( - <div key={userKey} className={'container'}> - <span className={'padding'}>{user.email}</span> + <div key={userKey} className="container"> + <span className="padding">{user.email}</span> <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> @@ -504,16 +210,16 @@ export class SharingManager extends React.Component<{}> { // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? ( - <div key={'owner'} className={'container'}> + <div key="owner" className="container"> <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>Owner</div> + <div className="permissions-dropdown">Owner</div> </div> </div> ) : null, sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( - <div key={'me'} className={'container'}> - <span className={'padding'}>Me</span> + <div key="me" className="container"> + <span className="padding">Me</span> <div className="edit-actions"> <div className={`permissions-dropdown-${curUserPermission}`}> {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} @@ -526,18 +232,27 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); - groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); + groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" }); const groupListContents = groupListMap.map(group => { - let groupKey = `acl-${StrCast(group.title)}`; + const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; return !permissions ? null : ( - <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> - <div className={'padding'}>{StrCast(group.title)}</div> + <div key={groupKey} className="container" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> + <div className="padding">{StrCast(group.title)}</div> - {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} - <div className={'edit-actions'}> + {group instanceof Doc ? ( + <IconButton + icon={<FontAwesomeIcon icon="info-circle" />} + size={Size.XSMALL} + color={SettingsManager.userColor} + onClick={action(() => { + GroupManager.Instance.currentGroup = group; + })} + /> + ) : null} + <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> {this.sharingOptions(uniform, group.title === 'Guest')} @@ -554,7 +269,14 @@ export class SharingManager extends React.Component<{}> { }); return ( <div className="sharing-interface"> - {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null} + {GroupManager.Instance?.currentGroup ? ( + <GroupMemberView + group={GroupManager.Instance.currentGroup} + onCloseButtonClick={action(() => { + GroupManager.Instance.currentGroup = undefined; + })} + /> + ) : null} <div className="sharing-contents" style={{ @@ -563,16 +285,16 @@ export class SharingManager extends React.Component<{}> { }}> <p className="share-title" style={{ color: SettingsManager.userColor }}> <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> - <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> + <FontAwesomeIcon icon="question-circle" size="sm" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> </div> <b>Share </b> {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} </p> <div className="share-copy-link"> - <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} /> + <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} /> </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} /> </div> {admin ? ( <div className="share-container"> @@ -614,19 +336,45 @@ export class SharingManager extends React.Component<{}> { </select> </div> <div className="share-button"> - <Button text={'SHARE'} type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} /> + <Button text="SHARE" type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} /> </div> </div> <div className="sort-checkboxes"> - <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label> - <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label> + <input + type="checkbox" + onChange={action(() => { + this.showUserOptions = !this.showUserOptions; + })} + />{' '} + <label style={{ marginRight: 10 }}>Individuals</label> + <input + type="checkbox" + onChange={action(() => { + this.showGroupOptions = !this.showGroupOptions; + })} + />{' '} + <label>Groups</label> </div> <div className="acl-container"> {Doc.noviceMode ? null : ( <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> <label>Upgrade Nested </label> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.upgradeNested = !this.upgradeNested; + })} + checked={this.upgradeNested} + />{' '} + <label>Upgrade Nested </label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> )} </div> @@ -635,14 +383,25 @@ export class SharingManager extends React.Component<{}> { <div className="share-container"> <div className="acl-container"> <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> </div> </div> )} <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}> - <div className={'individual-container'}> - <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> + <div className="individual-container"> + <div + className="user-sort" + onClick={action(() => { + this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-individual"> Individuals <IconButton @@ -654,11 +413,15 @@ export class SharingManager extends React.Component<{}> { </div> <div className="users-list">{userListContents}</div> </div> - <div className={'group-container'}> - <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + <div className="group-container"> + <div + className="user-sort" + onClick={action(() => { + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-group"> Groups - <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} @@ -666,7 +429,7 @@ export class SharingManager extends React.Component<{}> { /> </div> </div> - <div className={'groups-list'}>{groupListContents}</div> + <div className="groups-list">{groupListContents}</div> </div> </div> </div> @@ -674,7 +437,311 @@ export class SharingManager extends React.Component<{}> { ); } + /** + * Shares the document with a user. + */ + setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { + 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.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) { + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); + } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); + }); + }, 'set Doc permissions'); + + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ + setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { + const target = targetDoc || this.targetDoc!; + const acl = `acl-${normalizeEmail(StrCast(group.title))}`; + + 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 (group instanceof Doc) { + Doc.AddDocToList(group, 'docsShared', doc); + + this.users + .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) + .forEach(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists + }); + } + }); + }, 'set group permissions'); + /** + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. + */ + populateUsers = async () => { + if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { + this.populating = true; + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); + const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); + raw.map( + action((newUser: User) => { + const sharingDoc = docs[newUser.sharingDocumentId]; + const linkDatabase = docs[newUser.linkDatabaseId]; + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + if (!this.users.find(user => user.user.email === newUser.email)) { + this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); + // LinkManager.addLinkDB(linkDatabase); + } + } + }) + ); + this.populating = false; + } + }; + + // eslint-disable-next-line react/sort-comp + public close = action(() => { + this.isOpen = false; + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + // this.copied = false; + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + // eslint-disable-next-line react/no-unused-class-component-methods + public open = (target?: DocumentView, targetDoc?: Doc) => { + this.populateUsers(); + runInAction(() => { + this.targetDocView = target; + this.targetDoc = targetDoc || target?.Document; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Augment; + this.upgradeNested = true; + }); + }; + + /** + * Shares the documents shared with a group with a new user who has been added to that group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; + if (group.docsShared) { + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + } + } + }; + + /** + * Called from the properties sidebar to change permissions of a user. + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { + if (layout) this.layoutDocAcls = true; + if (shareWith !== 'Guest') { + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); + docs.forEach(doc => { + if (user) this.setInternalSharing(user, permission, doc); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); + }); + } else { + docs.forEach(doc => { + if (GetEffectiveAcl(doc) === AclAdmin) { + distributeAcls(`acl-${shareWith}`, permission, doc, undefined); + } + }); + } + this.layoutDocAcls = false; + }, 'sidebar set permissions'); + + /** + * Removes the documents shared with a user through a group when the user is removed from the group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeMember = (group: Doc, emailId: string) => { + const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; + + if (group.docsShared && user) { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + } + }; + + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeGroup = (group: Doc) => { + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => { + const acl = `acl-${StrCast(group.title)}`; + distributeAcls(acl, SharingPermissions.None, doc); + + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + + users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); + }); + } + }; + + // private setExternalSharing = (permission: string) => { + // const targetDoc = this.targetDoc; + // if (!targetDoc) { + // return; + // } + // targetDoc["acl-" + PublicKey] = permission; + // }s + + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = () => { + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); + }; + + private focusOn = (contents: string) => { + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; + const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.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> + ); + }; + + /** + * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. + */ + share = undoable( + action(() => { + if (this.selectedUsers) { + this.selectedUsers.forEach(user => { + if (user.value.includes(indType)) { + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); + } else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + + if (this.shareDocumentButtonRef.current) { + const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); + TaskCompletionBox.popupX = left - 1.5 * width; + TaskCompletionBox.popupY = top - 1.5 * height; + TaskCompletionBox.textDisplayed = 'Document shared!'; + TaskCompletionBox.taskCompleted = true; + setTimeout( + action(() => { + TaskCompletionBox.taskCompleted = false; + }), + 2000 + ); + } + + this.layoutDocAcls = false; + this.selectedUsers = null; + } + }), + 'share Doc' + ); + + /** + * Sorting algorithm to sort users. + */ + sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { + const { email: e1 } = u1.user; + const { email: e2 } = u2.user; + return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; + }; + + /** + * Sorting algorithm to sort groups. + */ + sortGroups = (group1: Doc, group2: Doc) => { + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); + return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; + }; + /** + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share + */ + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift('-multiple-'); + return dropdownValues.map(permission => ( + <option key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + )); + } + render() { - return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index eb47bbe88..3da85191f 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -10,6 +10,7 @@ export class SnappingManager { @observable _shiftKey = false; @observable _ctrlKey = false; @observable _metaKey = false; + @observable _showPresPaths = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized @@ -36,6 +37,7 @@ export class SnappingManager { public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore + public static get ShowPresPaths() { return this.Instance._showPresPaths; } // prettier-ignore public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore public static get IsDragging() { return this.Instance._isDragging; } // prettier-ignore public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore @@ -44,6 +46,7 @@ export class SnappingManager { public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore + public static SetShowPresPaths = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore public static SetIsDragging = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore public static SetIsResizing = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 4e941508d..956c0e674 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,8 +1,11 @@ +/* eslint-disable prefer-spread */ +/* eslint-disable no-use-before-define */ import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; import { RichTextField } from '../../fields/RichTextField'; -export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen +// eslint-disable-next-line prefer-const +let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -38,10 +41,11 @@ function propertyDecorator(target: any, key: string | symbol) { } export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { - return function () { + return function (...fargs) { const batch = UndoManager.StartBatch(batchName); try { - return fn.apply(undefined, arguments as any); + // eslint-disable-next-line prefer-rest-params + return fn.apply(undefined, fargs); } finally { batch.end(); } @@ -49,13 +53,15 @@ export function undoable(fn: (...args: any[]) => any, batchName: string): (...ar } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any; +// eslint-disable-next-line no-redeclare export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; +// eslint-disable-next-line no-redeclare export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { if (!key) { - return function () { + return function (...fargs: any[]) { const batch = UndoManager.StartBatch(''); try { - return target.apply(undefined, arguments); + return target.apply(undefined, fargs); } finally { batch.end(); } @@ -63,7 +69,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed } if (!descriptor) { propertyDecorator(target, key); - return; + return undefined; } const oldFunction = descriptor.value; @@ -87,14 +93,18 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - export let undoStackNames: string[] = observable([]); - export let redoStackNames: string[] = observable([]); - export let undoStack: UndoBatch[] = observable([]); - export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - export let batchCounter = observable.box(0); let undoing = false; - export let tempEvents: UndoEvent[] | undefined = undefined; + let tempEvents: UndoEvent[] | undefined; + export const undoStackNames: string[] = observable([]); + export const redoStackNames: string[] = observable([]); + export const undoStack: UndoBatch[] = observable([]); + export const redoStack: UndoBatch[] = observable([]); + export const batchCounter = observable.box(0); + let _fieldPrinter: (val: any) => string = val => val?.toString(); + export function SetFieldPrinter(printer: (val: any) => string) { + _fieldPrinter = printer; + } export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { @@ -103,8 +113,8 @@ export namespace UndoManager { ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + - ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) + ' = ' + // prettier-ignore + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value)) ); currentBatch.push(event); tempEvents?.push(event); @@ -130,21 +140,22 @@ export namespace UndoManager { } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; - const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); + const lastStack = UndoManager.undoStack.slice(-1)[0]; // .lastElement(); if (lastStack) { - lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); + lastStack.forEach(ev => { + fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1); + }); const fieldCount2: { [key: string]: number } = {}; - runInAction( - () => - (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { - if (fieldTypes.includes(ev.prop)) { - fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; - if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; - return false; - } - return true; - })) - ); + runInAction(() => { + UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + }); + }); } } export function TraceOpenBatches() { @@ -161,11 +172,10 @@ export namespace UndoManager { if (this.disposed) { console.log('WARNING: undo batch already disposed'); return false; - } else { - this.disposed = true; - openBatches.splice(openBatches.indexOf(this)); - return EndBatch(this.batchName, cancel); } + this.disposed = true; + openBatches.splice(openBatches.indexOf(this)); + return EndBatch(this.batchName, cancel); }; end = () => this.dispose(false); @@ -183,7 +193,7 @@ export namespace UndoManager { const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); - printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); @@ -200,10 +210,10 @@ export namespace UndoManager { export function StartTempBatch() { tempEvents = []; } - export function EndTempBatch<T>(success: boolean) { + export function EndTempBatch(success: boolean) { UndoManager.UndoTempBatch(success); } - //TODO Make this return the return value + // TODO Make this return the return value export function RunInBatch<T>(fn: () => T, batchName: string) { const batch = StartBatch(batchName); try { @@ -235,9 +245,11 @@ export namespace UndoManager { } undoing = true; - for (let i = commands.length - 1; i >= 0; i--) { - commands[i].undo(); - } + // eslint-disable-next-line prettier/prettier + commands + .slice() + .reverse() + .forEach(command => command.undo()); undoing = false; redoStackNames.push(names ?? '???'); @@ -256,9 +268,7 @@ export namespace UndoManager { } undoing = true; - for (const command of commands) { - command.redo(); - } + commands.forEach(command => command.redo()); undoing = false; undoStackNames.push(names ?? '???'); diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 02b3ee32c..2224e642d 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/media-has-caption */ +/* eslint-disable react/no-unused-class-component-methods */ import { Octokit } from '@octokit/core'; import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -13,7 +16,7 @@ import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { MainViewModal } from '../../views/MainViewModal'; -import '.././SettingsManager.scss'; +import '../SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; import './ReportManager.scss'; import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; @@ -25,10 +28,12 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d */ @observer export class ReportManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: ReportManager; @observable private isOpen = false; @observable private query = ''; + // eslint-disable-next-line react/sort-comp @action private setQuery = (q: string) => { this.query = q; }; @@ -83,7 +88,9 @@ export class ReportManager extends React.Component<{}> { this.formData = newData; }); - public close = action(() => (this.isOpen = false)); + public close = action(() => { + this.isOpen = false; + }); public open = action(async () => { this.isOpen = true; if (this.shownIssues.length === 0) { @@ -165,7 +172,7 @@ export class ReportManager extends React.Component<{}> { * @returns JSX element of a piece of media (image, video, audio) */ private getMediaPreview = (fileData: FileData): JSX.Element => { - const file = fileData.file; + const { file } = fileData; const mimeType = file.type; const preview = URL.createObjectURL(file); @@ -180,7 +187,8 @@ export class ReportManager extends React.Component<{}> { </div> </div> ); - } else if (mimeType.startsWith('video/')) { + } + if (mimeType.startsWith('video/')) { return ( <div key={fileData._id} className="report-media-wrapper"> <div className="report-media-content"> @@ -194,7 +202,8 @@ export class ReportManager extends React.Component<{}> { </div> </div> ); - } else if (mimeType.startsWith('audio/')) { + } + if (mimeType.startsWith('audio/')) { return ( <div key={fileData._id} className="report-audio-wrapper"> <audio src={preview} controls /> @@ -204,7 +213,7 @@ export class ReportManager extends React.Component<{}> { </div> ); } - return <></>; + return <div />; }; /** @@ -307,8 +316,8 @@ export class ReportManager extends React.Component<{}> { <div className="report-selects"> <Dropdown color={StrCast(Doc.UserDoc().userColor)} - formLabel={'Type'} - closeOnSelect={true} + formLabel="Type" + closeOnSelect items={bugDropdownItems} selectedVal={this.formData.type} setSelectedVal={val => { @@ -320,8 +329,8 @@ export class ReportManager extends React.Component<{}> { /> <Dropdown color={StrCast(Doc.UserDoc().userColor)} - formLabel={'Priority'} - closeOnSelect={true} + formLabel="Priority" + closeOnSelect items={priorityDropdownItems} selectedVal={this.formData.priority} setSelectedVal={val => { @@ -347,7 +356,7 @@ export class ReportManager extends React.Component<{}> { text="Submit" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} - icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} iconPlacement="right" onClick={() => { this.reportIssue(); @@ -364,7 +373,7 @@ export class ReportManager extends React.Component<{}> { /> )} <div style={{ position: 'absolute', top: '4px', right: '4px' }}> - <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} /> + <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={this.close} /> </div> </div> ); @@ -376,9 +385,8 @@ export class ReportManager extends React.Component<{}> { private reportComponent = () => { if (this.viewState === ViewState.VIEW) { return this.viewIssuesComponent(); - } else { - return this.reportIssueComponent(); } + return this.reportIssueComponent(); }; render() { @@ -386,7 +394,7 @@ export class ReportManager extends React.Component<{}> { <MainViewModal contents={this.reportComponent()} isDisplayed={this.isOpen} - interactive={true} + interactive closeOnExternalClick={this.close} dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: StrCast(Doc.UserDoc().userBackgroundColor), borderRadius: '8px' }} /> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 5760872fb..eb1030eec 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable, runInAction } from 'mobx'; @@ -17,6 +18,7 @@ export interface OriginalMenuProps { export interface SubmenuProps { description: string; + // eslint-disable-next-line no-use-before-define subitems: ContextMenuProps[]; noexpand?: boolean; addDivider?: boolean; @@ -37,7 +39,9 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & } componentDidMount() { - runInAction(() => (this._items.length = 0)); + runInAction(() => { + this._items.length = 0; + }); if ((this._props as SubmenuProps)?.subitems) { (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); } @@ -83,7 +87,9 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & return; } this.currentTimeout = setTimeout( - action(() => (this.overItem = false)), + action(() => { + this.overItem = false; + }), ContextMenuItem.timeout ); }; @@ -147,10 +153,10 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & ) : null} <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}> {this._props.description} - <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> + <FontAwesomeIcon icon="angle-right" size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> <div - className={`contextMenu-item-background`} + className="contextMenu-item-background" style={{ background: SnappingManager.userColor, }} @@ -159,5 +165,6 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & </div> ); } + return null; } } diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 14abd5f89..25415a4f0 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -48,10 +50,18 @@ export class DashboardView extends ObservableReactComponent<{}> { @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; @observable private newDashboardName = ''; @observable private newDashboardColor = '#AFAFAF'; - @action abortCreateNewDashboard = () => (this.openModal = false); - @action setNewDashboardName = (name: string) => (this.newDashboardName = name); - @action setNewDashboardColor = (color: string) => (this.newDashboardColor = color); - @action selectDashboardGroup = (group: DashboardGroup) => (this.selectedDashboardGroup = group); + @action abortCreateNewDashboard = () => { + this.openModal = false; + }; + @action setNewDashboardName = (name: string) => { + this.newDashboardName = name; + }; + @action setNewDashboardColor = (color: string) => { + this.newDashboardColor = color; + }; + @action selectDashboardGroup = (group: DashboardGroup) => { + this.selectedDashboardGroup = group; + }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { @@ -138,9 +148,9 @@ export class DashboardView extends ObservableReactComponent<{}> { <> <div className="dashboard-view"> <div className="left-menu"> - <Button text="My Dashboards" active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align={'flex-start'} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth /> + <Button text="My Dashboards" active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align="flex-start" onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth /> <Button - text={'Shared Dashboards' + ' (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'} + text={'Shared Dashboards (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'} active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards} color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color} align="flex-start" @@ -165,14 +175,22 @@ export class DashboardView extends ObservableReactComponent<{}> { onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}> <img + alt="" src={ href ?? 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU=' } /> <div className="info"> - <EditableText type={Type.PRIM} color={color} val={StrCast(dashboard.title)} setVal={val => (dashboard[DocData].title = val)} /> - {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} + <EditableText + type={Type.PRIM} + color={color} + val={StrCast(dashboard.title)} + setVal={val => { + dashboard[DocData].title = val; + }} + /> + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div />} <div className="more" onPointerDown={e => { @@ -208,7 +226,7 @@ export class DashboardView extends ObservableReactComponent<{}> { )} </div> </div> - <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive={true} closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} /> + <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} /> </> ); } @@ -252,6 +270,7 @@ export class DashboardView extends ObservableReactComponent<{}> { // } // CollectionView.SetSafeMode(true); } else if (state.nro || state.nro === null || state.readonly === false) { + /* empty */ } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { @@ -274,7 +293,7 @@ export class DashboardView extends ObservableReactComponent<{}> { public static resetDashboard = (dashboard: Doc) => { const config = StrCast(dashboard.dockingConfig); - const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const matches = config.match(/"documentId":"[a-z0-9-]+"/g); const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? []; const components = @@ -368,7 +387,7 @@ export class DashboardView extends ObservableReactComponent<{}> { title: `Untitled Tab 1`, }; - const title = name ? name : `Dashboard ${dashboardCount}`; + const title = name || `Dashboard ${dashboardCount}`; const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index e098bc361..b242acdba 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -7,14 +7,14 @@ import { MainViewModal } from './MainViewModal'; @observer export class DictationOverlay extends React.Component { + // eslint-disable-next-line no-use-before-define public static Instance: DictationOverlay; @observable private _dictationState = DictationManager.placeholder; @observable private _dictationSuccessState: boolean | undefined = undefined; @observable private _dictationDisplayState = false; @observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; - public isPointerDown = false; - public overlayTimeout: NodeJS.Timeout | undefined; + // eslint-disable-next-line react/no-unused-class-component-methods public hasActiveModal = false; constructor(props: any) { @@ -23,47 +23,32 @@ export class DictationOverlay extends React.Component { DictationOverlay.Instance = this; } - public initiateDictationFade = () => { - const duration = DictationManager.Commands.dictationFadeDuration; - this.overlayTimeout = setTimeout(() => { - this.dictationOverlayVisible = false; - this.dictationSuccess = undefined; - DictationOverlay.Instance.hasActiveModal = false; - setTimeout(() => (this.dictatedPhrase = DictationManager.placeholder), 500); - }, duration); - }; - public cancelDictationFade = () => { - if (this.overlayTimeout) { - clearTimeout(this.overlayTimeout); - this.overlayTimeout = undefined; - } - }; - - @computed public get dictatedPhrase() { - return this._dictationState; - } - @computed public get dictationSuccess() { - return this._dictationSuccessState; - } - @computed public get dictationOverlayVisible() { - return this._dictationDisplayState; - } - @computed public get isListening() { - return this._dictationListeningState; - } - + @computed public get dictatedPhrase() { return this._dictationState; } // prettier-ignore public set dictatedPhrase(value: string) { - runInAction(() => (this._dictationState = value)); + runInAction(() => { + this._dictationState = value; + }); } + @computed public get dictationSuccess() { return this._dictationSuccessState; } // prettier-ignore public set dictationSuccess(value: boolean | undefined) { - runInAction(() => (this._dictationSuccessState = value)); + runInAction(() => { this._dictationSuccessState = value; }); // prettier-ignore } + @computed public get dictationOverlayVisible() { return this._dictationDisplayState; } // prettier-ignore public set dictationOverlayVisible(value: boolean) { - runInAction(() => (this._dictationDisplayState = value)); + runInAction(() => { this._dictationDisplayState = value; }); // prettier-ignore } + @computed public get isListening() { return this._dictationListeningState; } // prettier-ignore public set isListening(value: DictationManager.Controls.ListeningUIStatus) { - runInAction(() => (this._dictationListeningState = value)); + runInAction(() => { this._dictationListeningState = value; }); // prettier-ignore } + public initiateDictationFade = () => { + setTimeout(() => { + this.dictationOverlayVisible = false; + this.dictationSuccess = undefined; + DictationOverlay.Instance.hasActiveModal = false; + setTimeout(() => { this.dictatedPhrase = DictationManager.placeholder; }, 500); // prettier-ignore + }, DictationManager.Commands.dictationFadeDuration); + }; render() { const success = this.dictationSuccess; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 348b18129..fae3610ca 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -326,7 +326,10 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { isAnyChildContentActive = () => this._isAnyChildContentActive; - whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + whenChildContentsActiveChanged = action((isActive: boolean) => { + this._isAnyChildContentActive = isActive; + this._props.whenChildContentsActiveChanged(isActive); + }); } return Component; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 16586d922..63b485a43 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core'; import { faCalendarDays } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -33,6 +36,7 @@ import { DashFieldView } from './nodes/formattedText/DashFieldView'; @observer export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> { private _dragRef = React.createRef<HTMLDivElement>(); + // eslint-disable-next-line no-use-before-define @observable public static Instance: DocumentButtonBar; constructor(props: any) { @@ -60,8 +64,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))} - onPointerLeave={action(e => (this.subPin = ''))} + onPointerEnter={action(() => { + this.subPin = allDocs ? 'All ' : ''; + })} + onPointerLeave={action(() => { + this.subPin = ''; + })} onClick={e => { this._props.views().forEach(dv => click(dv!.Document)); e.stopPropagation(); @@ -77,12 +85,14 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( <div className="documentButtonBar-icon documentButtonBar-follow" style={{ backgroundColor: followLink ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: followLink ? Colors.BLACK : Colors.WHITE }} - onClick={undoBatch(e => this._props.views().map(view => view?.toggleFollowLink(undefined, false)))}> + onClick={undoBatch(() => this._props.views().map(view => view?.toggleFollowLink(undefined, false)))}> <div className="documentButtonBar-followTypes"> {followBtn( true, - (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks), - (doc?: Doc) => (doc?.followAllLinks ? true : false), + (doc: Doc) => { + doc.followAllLinks = !doc.followAllLinks; + }, + (doc?: Doc) => !!doc?.followAllLinks, 'window-maximize' )} </div> @@ -100,22 +110,22 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( <div className="documentButtonBar-linkTypes"> <Tooltip title={<div>search for target</div>}> <div className="documentButtonBar-button"> - <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon={'link'} size="lg" /> + <button type="button" style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon="search" size="lg" /> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon="link" size="lg" /> </button> </div> </Tooltip> <Tooltip title={<div>open linked trail</div>}> <div className="documentButtonBar-button"> - <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleTrail}> + <button type="button" style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleTrail}> <FontAwesomeIcon icon="taxi" size="lg" /> </button> </div> </Tooltip> </div> <div style={{ width: 25, height: 25 }}> - <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn InMenu StartLink /> </div> </div> ); @@ -134,8 +144,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))} - onPointerLeave={action(e => (this.subEndLink = ''))} + onPointerEnter={action(() => { + this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : ''); + })} + onPointerLeave={action(() => { + this.subEndLink = ''; + })} onClick={e => { this.view0 && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.Document, true, this.view0, { @@ -157,7 +171,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( {linkBtn(false, true, 'address-card')} {linkBtn(true, true, 'id-card')} </div> - <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> + <DocumentLinksButton View={this.view0} AlwaysOn InMenu StartLink={false} /> </div> ); } @@ -177,15 +191,16 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action( - e => - (this.subPin = - (pinLayoutView ? 'Layout' : '') + - (pinLayoutView && pinContentView ? ' &' : '') + - (pinContentView ? ' Content View' : '') + - (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : '')) - )} - onPointerLeave={action(e => (this.subPin = ''))} + onPointerEnter={action(() => { + this.subPin = + (pinLayoutView ? 'Layout' : '') + + (pinLayoutView && pinContentView ? ' &' : '') + + (pinContentView ? ' Content View' : '') + + (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''); + })} + onPointerLeave={action(() => { + this.subPin = ''; + })} onClick={e => { const docs = this._props .views() @@ -232,8 +247,8 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get shareButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Open Sharing Manager'}</div>}> - <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> + <Tooltip title={<div className="dash-tooltip">Open Sharing Manager</div>}> + <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => SharingManager.Instance.open(this.view0, targetDoc)}> <FontAwesomeIcon className="documentdecorations-icon" icon="users" /> </div> </Tooltip> @@ -244,7 +259,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get menuButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}> + <Tooltip title={<div className="dash-tooltip">Open Context Menu</div>}> <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => this.openContextMenu(e))}> <FontAwesomeIcon className="documentdecorations-icon" icon="bars" /> </div> @@ -260,8 +275,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( <div className="documentButtonBar-icon" style={{ color: 'white' }} - onClick={e => { - console.log('hi: ', CalendarManager.Instance); + onClick={() => { CalendarManager.Instance.open(this.view0, targetDoc); }}> <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} /> @@ -282,7 +296,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }} onPointerDown={action((e: React.PointerEvent) => { this._isRecording = true; - this._props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction)); + this._props.views().map( + view => + view && + DocumentViewInternal.recordAudioAnnotation( + view.dataDoc, + view.LayoutFieldKey, + stopFunc => { + this._stopFunc = stopFunc; + }, + emptyFunction + ) + ); const b = UndoManager.StartBatch('Recording'); setupMoveUpEvents( this, @@ -310,10 +335,10 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.Document]); - const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); + const origin = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); dragData.defaultDropAction = dropActionType.embed; dragData.canEmbed = true; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, origin[0], origin[1], { hideSource: false }); return true; } return false; @@ -336,8 +361,19 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @computed get templateButton() { return !this.view0 ? null : ( - <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> - <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> + <Tooltip + title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} + open={this._tooltipOpen} + onClose={action(() => { + this._tooltipOpen = false; + })} + placement="bottom"> + <div + className="documentButtonBar-linkFlyout" + ref={this._dragRef} + onPointerEnter={action(() => { + !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true); + })}> <Popup icon={<FaEdit />} popup={this.templateMenu} popupContainsPt={returnTrue} /> </div> </Tooltip> @@ -365,17 +401,17 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @observable _captureEndLinkLayout = false; @action - captureEndLinkLayout = (e: React.PointerEvent) => { + captureEndLinkLayout = () => { this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @observable _captureEndLinkContent = false; @action - captureEndLinkContent = (e: React.PointerEvent) => { + captureEndLinkContent = () => { this._captureEndLinkContent = !this._captureEndLinkContent; }; @action - captureEndLinkState = (e: React.PointerEvent) => { + captureEndLinkState = () => { this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @@ -402,13 +438,15 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( return ( <div className="documentButtonBar"> <div className="documentButtonBar-button"> - <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} ShowCount={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn InMenu ShowCount /> </div> {this._showLinkPopup ? ( <div style={{ position: 'absolute', zIndex: 1000 }}> <LinkPopup key="popup" - linkCreated={link => (link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick))} + linkCreated={link => { + link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick); + }} linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this._props.views().lastElement()?.Document} /> @@ -423,7 +461,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( <div className="documentButtonBar-button">{this.pinButton}</div> <div className="documentButtonBar-button">{this.recordButton}</div> <div className="documentButtonBar-button">{this.calendarButton}</div> - {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} + {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} <div className="documentButtonBar-button">{this.menuButton}</div> </div> ); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 85e893e19..684b948af 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -70,7 +72,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> { constructor(props: EditableProps) { super(props); makeObservable(this); - this._editing = this._props.editing ? true : false; + this._editing = !!this._props.editing; } componentDidMount(): void { @@ -166,7 +168,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> { this._props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; } - + // eslint-disable-next-line no-fallthrough default: if (this._props.textCallback?.(e.key)) { e.stopPropagation(); @@ -186,7 +188,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> { this._editing = true; this._props.isEditingCallback?.(true); } - // e.stopPropagation(); } }; @@ -223,6 +224,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> { renderEditor() { return this._props.autosuggestProps ? ( <Autosuggest + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props.autosuggestProps.autosuggestProps} inputProps={{ className: 'editableView-input', @@ -241,12 +243,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> { ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? ( <input className="editableView-input" - ref={r => (this._inputref = r)} + ref={r => { this._inputref = r; }} // prettier-ignore style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }} placeholder={this._props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this._props.GetValue()} - autoFocus={true} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus onChange={this.onChange} onKeyDown={this.onKeyDown} onPointerDown={this.stopPropagation} @@ -256,12 +259,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> { ) : ( <textarea className="editableView-input" - ref={r => (this._inputref = r)} + ref={r => { this._inputref = r; }} // prettier-ignore style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this._props.background }} placeholder={this._props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this._props.GetValue()} - autoFocus={true} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus onChange={this.onChange} onKeyDown={this.onKeyDown} onPointerDown={this.stopPropagation} @@ -304,7 +308,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> { fontStyle: this._props.fontStyle, fontSize: this._props.fontSize, }}> - {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : ''} + { + // eslint-disable-next-line react/jsx-props-no-spreading + this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : '' + } </span> </div> ); diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx index 6a5c2cb4c..3cb7848c2 100644 --- a/src/client/views/FieldsDropdown.tsx +++ b/src/client/views/FieldsDropdown.tsx @@ -64,7 +64,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps return ( <Select styles={{ - control: (baseStyles, state) => ({ + control: (baseStyles /* , state */) => ({ ...baseStyles, minHeight: '5px', maxHeight: '30px', @@ -73,17 +73,17 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps padding: 0, margin: 0, }), - singleValue: (baseStyles, state) => ({ + singleValue: (baseStyles /* , state */) => ({ ...baseStyles, color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor, }), - placeholder: (baseStyles, state) => ({ + placeholder: (baseStyles /* , state */) => ({ ...baseStyles, color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor, }), - input: (baseStyles, state) => ({ + input: (baseStyles /* , state */) => ({ ...baseStyles, padding: 0, margin: 0, @@ -95,7 +95,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps color: SettingsManager.userColor, background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, }), - menuList: (baseStyles, state) => ({ + menuList: (baseStyles /* , state */) => ({ ...baseStyles, backgroundColor: SettingsManager.userBackgroundColor, }), @@ -106,12 +106,14 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps onChange={val => this._props.selectFunc((val as any as { value: string; label: string }).value)} onKeyDown={e => { if (e.key === 'Enter') { - runInAction(() => this._props.selectFunc((this._newField = (e.nativeEvent.target as any)?.value))); + runInAction(() => { + this._props.selectFunc((this._newField = (e.nativeEvent.target as any)?.value)); + }); } e.stopPropagation(); }} onMenuClose={this._props.menuClose} - closeMenuOnSelect={true} + closeMenuOnSelect value={this._props.showPlaceholder ? null : undefined} /> ); diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index d3cebd1c3..be307600c 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, makeObservable, observable, ObservableMap } from 'mobx'; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 5c923d301..ebd61db7d 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -12,7 +12,7 @@ import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; -import { UndoManager } from '../util/UndoManager'; +import { UndoManager, undoable } from '../util/UndoManager'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { InkStrokeProperties } from './InkStrokeProperties'; @@ -27,15 +27,13 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { AnchorMenu } from './pdf/AnchorMenu'; const modifiers = ['control', 'meta', 'shift', 'alt']; -type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; type KeyControlInfo = { preventDefault: boolean; stopPropagation: boolean; }; - -export let CtrlKey = false; +type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; export class KeyManager { - public static Instance: KeyManager = new KeyManager(); + public static Instance = new KeyManager(); private router = new Map<string, KeyHandler>(); constructor() { @@ -49,8 +47,8 @@ export class KeyManager { this.router.set('1000', this.shift); } - public unhandle = action((e: KeyboardEvent) => { - e.key === 'Control' && (CtrlKey = false); + public unhandle = action((/* e: KeyboardEvent */) => { + /* empty */ }); public handleModifiers = action((e: KeyboardEvent) => { if (e.shiftKey) SnappingManager.SetShiftKey(true); @@ -65,10 +63,9 @@ export class KeyManager { public handle = action((e: KeyboardEvent) => { // accumulate buffer of characters to insert in a new text note. once the note is created, it will stop keyboard events from reaching this function. - if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key); - e.key === 'Control' && (CtrlKey = true); + if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar += e.key === 'Enter' ? '\n' : e.key; const keyname = e.key && e.key.toLowerCase(); - this.handleGreedy(keyname); + this.handleGreedy(/* keyname */); if (modifiers.includes(keyname)) { return; @@ -101,20 +98,15 @@ export class KeyManager { case 'u': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { const ungroupings = SelectionManager.Views; - UndoManager.RunInBatch(() => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup'); + undoable(() => () => ungroupings.forEach(dv => { dv.layoutDoc.group = undefined; }), 'ungroup'); SelectionManager.DeselectAll(); } break; case 'g': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { const selected = SelectionManager.Views; - const collectionView = selected.reduce((col, dv) => (col === null || dv.CollectionFreeFormView === col ? dv.CollectionFreeFormView : undefined), null as null | undefined | CollectionFreeFormView); - if (collectionView) { - UndoManager.RunInBatch(() => - collectionView._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs) - , 'grouping'); - break; - } + const cv = selected.reduce((col, dv) => (!col || dv.CollectionFreeFormView === col ? dv.CollectionFreeFormView : undefined), undefined as undefined | CollectionFreeFormView); + cv && undoable(() => cv._marqueeViewRef.current?.collection(e, true, SelectionManager.Docs), 'grouping'); } break; case ' ': @@ -146,7 +138,7 @@ export class KeyManager { // DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); - if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close(); + if (!GroupManager.Instance.isOpen) SettingsManager.Instance.closeMgr(); GroupManager.Instance.close(); window.getSelection()?.empty(); document.body.focus(); @@ -186,17 +178,18 @@ export class KeyManager { case 'arrowdown': return this.nudge(0, 10, 'nudge down'); case 'u' : if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { - UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => (doc.group = undefined)), 'unggroup'); + UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = undefined}), 'unggroup'); SelectionManager.DeselectAll(); } break; case 'g': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { const randomGroup = random(0, 1000); - UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => (doc.group = randomGroup)), 'group'); + UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => {doc.group = randomGroup}), 'group'); SelectionManager.DeselectAll(); } break; + default: } // prettier-ignore return { @@ -212,8 +205,9 @@ export class KeyManager { switch (keyname) { case 'ƒ': case 'f': - const dv = SelectionManager.Views?.[0]; - UndoManager.RunInBatch(() => dv.CollectionFreeFormDocumentView?.float(), 'float'); + UndoManager.RunInBatch(() => SelectionManager.Views?.[0]?.CollectionFreeFormDocumentView?.float(), 'float'); + break; + default: } return { @@ -248,15 +242,19 @@ export class KeyManager { PromiseValue(Cast(Doc.UserDoc()['tabs-button-tools'], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); break; case 'i': - const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); - if (importBtn) { - MainView.Instance.selectMenu(importBtn); + { + const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); + if (importBtn) { + MainView.Instance.selectMenu(importBtn); + } } break; case 's': - const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); - if (trailsBtn) { - MainView.Instance.selectMenu(trailsBtn); + { + const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); + if (trailsBtn) { + MainView.Instance.selectMenu(trailsBtn); + } } break; case 'f': @@ -323,6 +321,7 @@ export class KeyManager { } preventDefault = false; break; + default: } return { @@ -357,6 +356,7 @@ export class KeyManager { case 'p': Doc.ActiveTool = InkTool.Write; break; + default: } return { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 9f82fdb18..e04daec3b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -46,6 +46,7 @@ import { PresBox } from './nodes/trails'; import { StyleProp } from './StyleProvider'; const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore + @observer export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big) @@ -66,7 +67,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() this._props.setContentViewBox?.(this); this._disposers.selfDisper = reaction( () => this._props.isSelected(), // react to stroke being deselected by turning off ink handles - selected => !selected && (InkStrokeProperties.Instance._controlButton = false) + selected => { + !selected && (InkStrokeProperties.Instance._controlButton = false); + } ); } componentWillUnmount() { @@ -168,7 +171,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() }), isEditing, isEditing, - action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)) + action(() => { + wasSelected && (InkStrokeProperties.Instance._currentPoint = -1); + }) ); }; @@ -332,26 +337,28 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() }; _subContentView: ViewBoxInterface | undefined; - setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc); - @computed get fillColor() { + setSubContentView = (doc: ViewBoxInterface) => { + this._subContentView = doc; + }; + @computed get fillColor(): string { const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask); return isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FillColor) ?? 'transparent'; } @computed get strokeColor() { const { inkData } = this.inkScaledData(); - const fillColor = this.fillColor; + const { fillColor } = this; return !InkingStroke.IsClosed(inkData) && fillColor && fillColor !== 'transparent' ? fillColor : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); } render() { TraceMobx(); - const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData(); + const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY } = this.inkScaledData(); const startMarker = StrCast(this.layoutDoc.stroke_startMarker); const endMarker = StrCast(this.layoutDoc.stroke_endMarker); const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1); const closed = InkingStroke.IsClosed(inkData); const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask); - const fillColor = this.fillColor; + const { fillColor } = this; // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. if (isInkMask && (this.layoutDoc._width !== Math.round(NumCast(this.layoutDoc._width)) || this.layoutDoc._height !== Math.round(NumCast(this.layoutDoc._height)))) { @@ -392,7 +399,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() ); const higlightMargin = Math.min(12, Math.max(2, 0.3 * inkStrokeWidth)); // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) => + const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false): any => InteractionUtils.CreatePolyline( inkData, inkLeft, @@ -420,16 +427,24 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size. // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429 // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/ - const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', ''); + // const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', ''); const interactions = { - onPointerLeave: action(() => (this._nearestScrPt = undefined)), + onPointerLeave: action(() => { + this._nearestScrPt = undefined; + }), onPointerMove: this._props.isSelected() ? this.onPointerMove : undefined, onClick: (e: React.MouseEvent) => this._handledClick && e.stopPropagation(), onContextMenu: () => { const cm = ContextMenu.Instance; !Doc.noviceMode && cm?.addItem({ description: 'Recognize Writing', event: this.analyzeStrokes, icon: 'paint-brush' }); cm?.addItem({ description: 'Toggle Mask', event: () => InkingStroke.toggleMask(this.dataDoc), icon: 'paint-brush' }); - cm?.addItem({ description: 'Edit Points', event: action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)), icon: 'paint-brush' }); + cm?.addItem({ + description: 'Edit Points', + event: action(() => { + InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton; + }), + icon: 'paint-brush', + }); }, }; return ( @@ -441,6 +456,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', cursor: this._props.isSelected() ? 'default' : undefined, }} + // eslint-disable-next-line react/jsx-props-no-spreading {...interactions}> {clickableLine(this.onPointerDown, isInkMask)} {isInkMask ? null : inkLine} @@ -454,18 +470,19 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() width: NumCast(this.layoutDoc._width), transform: `scale(${this._props.NativeDimScaling?.() || 1})`, transformOrigin: 'top left', - //top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2, + // top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2, }}> <FormattedTextBox + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setHeight={undefined} setContentViewBox={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text) yPadding={10} xPadding={10} fieldKey="text" - //dontRegisterView={true} - noSidebar={true} - dontScale={true} + // dontRegisterView={true} + noSidebar + dontScale isContentActive={this._props.isContentActive} /> </div> @@ -474,37 +491,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() ); } } - -export function SetActiveInkWidth(width: string): void { - !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); -} -export function SetActiveBezierApprox(bezier: string): void { - ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); -} -export function SetActiveInkColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeInkColor = value); -} -export function SetActiveIsInkMask(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); -} -export function SetActiveInkHideTextLabels(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); -} -export function SetActiveFillColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeFillColor = value); -} -export function SetActiveArrowStart(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); -} -export function SetActiveArrowEnd(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); -} -export function SetActiveArrowScale(value: number) { - ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); -} -export function SetActiveDash(dash: string): void { - !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); -} export function ActiveInkPen(): Doc { return Doc.UserDoc(); } @@ -538,3 +524,34 @@ export function ActiveInkWidth(): number { export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } + +export function SetActiveInkWidth(width: string): void { + !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); +} +export function SetActiveBezierApprox(bezier: string): void { + ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); +} +export function SetActiveInkColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeInkColor = value); +} +export function SetActiveIsInkMask(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); +} +export function SetActiveInkHideTextLabels(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); +} +export function SetActiveFillColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeFillColor = value); +} +export function SetActiveArrowStart(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); +} +export function SetActiveArrowEnd(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); +} +export function SetActiveArrowScale(value: number) { + ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); +} +export function SetActiveDash(dash: string): void { + !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); +} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b0156846f..a13fe1cb7 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable node/no-unpublished-import */ import { library } from '@fortawesome/fontawesome-svg-core'; import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as far from '@fortawesome/free-regular-svg-icons'; @@ -6,9 +7,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +// eslint-disable-next-line import/no-relative-packages import '../../../node_modules/browndash-components/dist/styles/global.min.css'; import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils'; -import { Utils, emptyFunction } from '../../Utils'; +import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { DocCast, StrCast } from '../../fields/Types'; @@ -625,8 +627,8 @@ export class MainView extends ObservableReactComponent<{}> { waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined); headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); - addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); - removeHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); + addHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); + removeHeaderDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); @computed get headerBarDocView() { return ( <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}> @@ -734,8 +736,8 @@ export class MainView extends ObservableReactComponent<{}> { setupMoveUpEvents( this, e, - action(e => { - this._leftMenuFlyoutWidth = Math.max(e.clientX - 58, 0); + action(ev => { + this._leftMenuFlyoutWidth = Math.max(ev.clientX - 58, 0); return false; }), () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(), @@ -827,7 +829,7 @@ export class MainView extends ObservableReactComponent<{}> { if (willOpen) { switch ((this._panelContent = title)) { case 'Settings': - SettingsManager.Instance.open(); + SettingsManager.Instance.openMgr(); break; case 'Help': break; @@ -907,19 +909,19 @@ export class MainView extends ObservableReactComponent<{}> { // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); this._sidebarContent.proto = DocCast(button.target); - SettingsManager.Instance.SetLastPressedBtn(button); + SettingsManager.Instance.LastPressedBtn = button; }); closeFlyout = action(() => { - SettingsManager.Instance.SetLastPressedBtn(undefined); + SettingsManager.Instance.LastPressedBtn = undefined; this._panelContent = 'none'; this._sidebarContent.proto = undefined; this._leftMenuFlyoutWidth = 0; }); - remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); - moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); - addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true); + remButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); + moveButtonDoc = (docs: Doc | Doc[], targetCol: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(docs) && addDocument(docs); + addButtonDoc = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true); buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); @@ -965,14 +967,16 @@ export class MainView extends ObservableReactComponent<{}> { return !dragPar?.layoutDoc.freeform_snapLines ? null : ( <div className="mainView-snapLines"> <svg style={{ width: '100%', height: '100%' }}> - {SnappingManager.HorizSnapLines.map((l, i) => ( - // eslint-disable-next-line react/no-array-index-key - <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} /> - ))} - {SnappingManager.VertSnapLines.map((l, i) => ( - // eslint-disable-next-line react/no-array-index-key - <line key={i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} /> - ))} + {[ + ...SnappingManager.HorizSnapLines.map((l, i) => ( + // eslint-disable-next-line react/no-array-index-key + <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" /> + )), + ...SnappingManager.VertSnapLines.map((l, i) => ( + // eslint-disable-next-line react/no-array-index-key + <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.layoutDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" /> + )), + ]} </svg> </div> ); diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx deleted file mode 100644 index 9626f1653..000000000 --- a/src/client/views/MetadataEntryMenu.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { IReactionDisposer, action, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import * as Autosuggest from 'react-autosuggest'; -import { emptyFunction, emptyPath } from '../../Utils'; -import { Doc, DocListCast, Field, FieldType } from '../../fields/Doc'; -import { undoBatch } from '../util/UndoManager'; -import './MetadataEntryMenu.scss'; -import { KeyValueBox } from './nodes/KeyValueBox'; - -export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>; -export interface MetadataEntryProps { - docs: Doc[]; - onError?: () => boolean; -} - -@observer -export class MetadataEntryMenu extends React.Component<MetadataEntryProps> { - @observable private _currentKey: string = ''; - @observable private _currentValue: string = ''; - private _addChildren: boolean = false; - @observable _allSuggestions: string[] = []; - _suggestionDispser: IReactionDisposer | undefined; - private userModified = false; - - private autosuggestRef = React.createRef<Autosuggest>(); - - @action - onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { - this._currentKey = newValue; - if (!this.userModified) { - this.previewValue(); - } - }; - - previewValue = async () => { - let field: FieldType | undefined | null = null; - let onProto: boolean = false; - let value: string | undefined; - const { docs } = this.props; - for (const doc of docs) { - const v = await doc[this._currentKey]; - onProto = onProto || !Object.keys(doc).includes(this._currentKey); - if (field === null) { - field = v; - } else if (v !== field) { - value = 'multiple values'; - } - } - if (value === undefined) { - if (field !== null && field !== undefined) { - value = (onProto ? '' : '= ') + Field.toScriptString(field); - } else { - value = ''; - } - } - const s = value; - runInAction(() => (this._currentValue = s)); - }; - - @action - onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._currentValue = e.target.value; - this.userModified = e.target.value.trim() !== ''; - }; - - @undoBatch - @action - onValueKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.stopPropagation(); - const script = KeyValueBox.CompileKVPScript(this._currentValue); - if (!script) return; - - let childSuccess = true; - if (this._addChildren) { - for (const document of this.props.docs) { - const collectionChildren = DocListCast(document.data); - if (collectionChildren) { - childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script)); - } - } - } - const success = this.props.docs.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess; - if (!success) { - if (this.props.onError) { - if (this.props.onError()) { - this.clearInputs(); - } - } else { - this.clearInputs(); - } - } else { - this.clearInputs(); - } - } - }; - - @action - clearInputs = () => { - this._currentKey = ''; - this._currentValue = ''; - this.userModified = false; - if (this.autosuggestRef.current) { - const input: HTMLInputElement = (this.autosuggestRef.current as any).input; - input && input.focus(); - } - }; - - getKeySuggestions = (value: string) => { - value = value.toLowerCase(); - const docs = this.props.docs; - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); - }; - getSuggestionValue = (suggestion: string) => suggestion; - - renderSuggestion = (suggestion: string) => { - return null; - }; - componentDidMount() { - this._suggestionDispser = reaction( - () => this._currentKey, - () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)), - { fireImmediately: true } - ); - } - componentWillUnmount() { - this._suggestionDispser && this._suggestionDispser(); - } - - onClick = (e: React.ChangeEvent<HTMLInputElement>) => { - this._addChildren = !this._addChildren; - }; - - private get considerChildOptions() { - if (!this.props.docs.every(doc => doc._type_collection !== undefined)) { - return null; - } - return ( - <div style={{ display: 'flex' }}> - Children: - <input type="checkbox" onChange={this.onClick}></input> - </div> - ); - } - - _ref = React.createRef<HTMLInputElement>(); - render() { - return ( - <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> - <div className="metadataEntry-inputArea"> - <div style={{ display: 'flex', flexDirection: 'row' }}> - <span>Key:</span> - <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}> - <Autosuggest - // @ts-ignore - inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={emptyPath} - alwaysRenderSuggestions={false} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={emptyFunction} - onSuggestionsClearRequested={emptyFunction} - ref={this.autosuggestRef} - /> - </div> - </div> - <div style={{ display: 'flex', flexDirection: 'row' }}> - <span>Value:</span> - <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> - </div> - {this.considerChildOptions} - </div> - <div className="metadataEntry-keys"> - <ul> - {this._allSuggestions - .slice() - .sort() - .map(s => ( - <li - key={s} - onClick={action(() => { - this._currentKey = s; - this.previewValue(); - })}> - {s} - </li> - ))} - </ul> - </div> - </div> - ); - } -} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 6539c0834..960282a08 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -18,6 +18,7 @@ import { ObservableReactComponent } from './ObservableReactComponent'; import './OverlayView.scss'; import { DefaultStyleProvider } from './StyleProvider'; import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; + const _global = (window /* browser */ || global) /* node */ as any; export type OverlayDisposer = () => void; @@ -82,12 +83,12 @@ export class OverlayWindow extends ObservableReactComponent<OverlayWindowProps> this.height = Math.max(this.height, 30); }; - onPointerUp = (e: PointerEvent) => { + onPointerUp = () => { document.removeEventListener('pointermove', this.onPointerMove); document.removeEventListener('pointerup', this.onPointerUp); }; - onResizerPointerUp = (e: PointerEvent) => { + onResizerPointerUp = () => { document.removeEventListener('pointermove', this.onResizerPointerMove); document.removeEventListener('pointerup', this.onResizerPointerUp); }; @@ -171,7 +172,7 @@ export class OverlayView extends ObservableReactComponent<{}> { {contents} </OverlayWindow> ); - this._elements.push(contents); + this._elements.push(wincontents); return remove; } diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index c7720d834..d83fea2a5 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable react/no-unused-class-component-methods */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; @@ -12,7 +15,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit import { RxWidth } from 'react-icons/rx'; import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; import { TfiBarChart } from 'react-icons/tfi'; -import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast } from '../../fields/Types'; @@ -32,6 +35,7 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @observer export class PropertiesButtons extends React.Component<{}, {}> { + // eslint-disable-next-line no-use-before-define @observable public static Instance: PropertiesButtons; @computed get selectedDoc() { @@ -44,61 +48,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !SelectionManager.SelectedSchemaDoc && SelectionManager.Views.lastElement()?.topMost; } - propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { - const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedLayoutDoc; - const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = !(dv?.layoutDoc || doc)[prop]); - return !targetDoc ? null : ( - <Toggle - toggleStatus={BoolCast(targetDoc[property])} - tooltip={tooltip(BoolCast(targetDoc[property]))} - text={label(targetDoc?.[property])} - color={SettingsManager.userColor} - icon={icon(targetDoc?.[property] as any)} - iconPlacement="left" - align="flex-start" - fillWidth={true} - toggleType={ToggleType.BUTTON} - onClick={undoable(() => { - if (SelectionManager.Views.length > 1) { - SelectionManager.Views.forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property)); - } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); - }, property)} - /> - ); - }; - - // this implments a container pattern by marking the targetDoc (collection) as a lightbox - // that always fits its contents to its container and that hides all other documents when - // a link is followed that targets a 'lightbox' destination - @computed get isLightboxButton() { - return this.propertyToggleBtn( - on => 'Lightbox', - 'isLightbox', - on => `${on ? 'Set' : 'Remove'} lightbox flag`, - on => 'window-restore', - onClick => { - SelectionManager.Views.forEach(dv => { - const containerDoc = dv.Document; - //containerDoc.followAllLinks = - // containerDoc.noShadow = - // containerDoc.layout_disableBrushing = - // containerDoc._forceActive = - //containerDoc._freeform_fitContentsToBox = - containerDoc._isLightbox = !containerDoc._isLightbox; - //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; - const containerContents = DocListCast(dv.dataDoc[Doc.LayoutFieldKey(containerDoc)]); - //dv.Docuemnt.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' }); - containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false))); - }); - } - ); - } - @computed get titleButton() { return this.propertyToggleBtn( - on => (!on ? 'SHOW TITLE' : this.selectedDoc?.['_layout_showTitle'] === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'), + on => (!on ? 'SHOW TITLE' : this.selectedDoc?._layout_showTitle === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'), '_layout_showTitle', - on => 'Switch between title styles', + () => 'Switch between title styles', on => (on ? <MdSubtitlesOff /> : <MdSubtitles />), // {currentIcon}, //(on ? <MdSubtitles/> :) , //,'text-width', on ? <MdSubtitles/> : <MdSubtitlesOff/>, (dv, doc) => { const tdoc = dv?.Document || doc; @@ -119,7 +73,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { } @computed get maskButton() { - //highlight text while going down and reading through + // highlight text while going down and reading through return this.propertyToggleBtn( on => (on ? 'PLAIN INK' : 'HIGHLIGHTER MASK'), 'stroke_isInkMask', @@ -132,10 +86,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get hideImageButton() { // put in developer -- can trace on top of object and drawing is still there return this.propertyToggleBtn( - on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), //'Background', + on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), // 'Background', '_hideImage', on => (on ? 'Show Image' : 'Show Background'), - on => (on ? <BiShow /> : <BiHide />) //'portrait' + on => (on ? <BiShow /> : <BiHide />) // 'portrait' ); } @@ -144,35 +98,35 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => (on ? 'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'), '_freeform_useClusters', on => `${on ? 'Hide' : 'Show'} clusters`, - on => <FaBraille /> + () => <FaBraille /> ); } @computed get panButton() { return this.propertyToggleBtn( - on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), //'Lock\xA0View', + on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), // 'Lock\xA0View', '_lockedTransform', on => `${on ? 'Unlock' : 'Lock'} panning of view`, - on => (on ? <TbHandStop /> : <TbHandOff />) //'lock' + on => (on ? <TbHandStop /> : <TbHandOff />) // 'lock' ); } @computed get forceActiveButton() { - //select text + // select text return this.propertyToggleBtn( on => (on ? 'SELECT TO INTERACT' : 'ALWAYS INTERACTIVE'), '_forceActive', on => `${on ? 'Document must be selected to interact with its contents' : 'Contents always active (respond to click/drag events)'} `, - on => <MdTouchApp /> // 'eye' + () => <MdTouchApp /> // 'eye' ); } @computed get verticalAlignButton() { - //select text + // select text return this.propertyToggleBtn( on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'), '_layout_centered', on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `, - on => <MdTouchApp /> // 'eye' + () => <MdTouchApp /> // 'eye' ); } @@ -192,10 +146,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get fitContentButton() { return this.propertyToggleBtn( - on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All', + on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), // 'View All', '_freeform_fitContentsToBox', on => `${on ? "Don't" : 'Do'} fit content to container visible area`, - on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) //'object-group' + on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) // 'object-group' ); } @@ -228,7 +182,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get layout_fitWidthButton() { return this.propertyToggleBtn( - on => (on ? 'SCALED VIEW' : 'READING VIEW'), //'Fit\xA0Width', + on => (on ? 'SCALED VIEW' : 'READING VIEW'), // 'Fit\xA0Width', '_layout_fitWidth', on => on @@ -240,12 +194,14 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get captionButton() { return this.propertyToggleBtn( - //DEVELOPER - on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), //'Caption', + // DEVELOPER + on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), // 'Caption', '_layout_showCaption', on => `${on ? 'Hide' : 'Show'} caption footer`, - on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), //'closed-captioning', - (dv, doc) => ((dv?.Document || doc)._layout_showCaption = (dv?.Document || doc)._layout_showCaption === undefined ? 'caption' : undefined) + on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), // 'closed-captioning', + (dv, doc) => { + (dv?.Document || doc)._layout_showCaption = (dv?.Document || doc)._layout_showCaption === undefined ? 'caption' : undefined; + } ); } @@ -256,7 +212,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { '_chromeHidden', on => `${on ? 'Show' : 'Hide'} editing UI`, on => (on ? <TbEditCircle /> : <TbEditCircleOff />), // 'edit', - (dv, doc) => ((dv?.Document || doc)._chromeHidden = !(dv?.Document || doc)._chromeHidden) + (dv, doc) => { + (dv?.Document || doc)._chromeHidden = !(dv?.Document || doc)._chromeHidden; + } ); } @@ -265,8 +223,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn( on => (on ? 'AUTO\xA0SIZE' : 'FIXED SIZE'), '_layout_autoHeight', - on => `Automatical vertical sizing to show all content`, - on => <FontAwesomeIcon icon="arrows-alt-v" size="lg" /> + () => `Automatical vertical sizing to show all content`, + () => <FontAwesomeIcon icon="arrows-alt-v" size="lg" /> ); } @@ -274,8 +232,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn( on => (on ? 'HIDE GRID' : 'DISPLAY GRID'), '_freeform_backgroundGrid', - on => `Display background grid in collection`, - on => (on ? <MdGridOff /> : <MdGridOn />) //'border-all' + () => `Display background grid in collection`, + on => (on ? <MdGridOff /> : <MdGridOn />) // 'border-all' ); } @@ -317,8 +275,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn( on => (on ? 'HIDE SNAP LINES' : 'SHOW SNAP LINES'), 'freeform_snapLines', - on => `Display snapping lines when objects are dragged`, - on => <TfiBarChart />, //'th', + () => `Display snapping lines when objects are dragged`, + () => <TfiBarChart />, // 'th', undefined ); } @@ -361,7 +319,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { @undoBatch handlePerspectiveChange = (e: any) => { this.selectedDoc && (this.selectedDoc._type_collection = e.target.value); - SelectionManager.Views.forEach(docView => (docView.layoutDoc._type_collection = e.target.value)); + SelectionManager.Views.forEach(docView => { + docView.layoutDoc._type_collection = e.target.value; + }); }; @computed get onClickVal() { const linkButton = IsFollowLinkScript(this.selectedDoc.onClick); @@ -369,10 +329,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace'; - else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight'; - else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal'; - else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail'; - else return 'nothing'; + if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight'; + if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal'; + if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail'; + return 'nothing'; } @computed @@ -385,20 +345,18 @@ export class PropertiesButtons extends React.Component<{}, {}> { ['linkOnRight', 'Open Link on Right'], ]; - const items: IListItemProps[] = buttonList.map(value => { - return { - text: value[1], - val: value[1], - }; - }); + const items: IListItemProps[] = buttonList.map(value => ({ + text: value[1], + val: value[1], + })); return !this.selectedDoc ? null : ( <Dropdown - tooltip={'Choose onClick behavior'} + tooltip="Choose onClick behavior" items={items} - closeOnSelect={true} + closeOnSelect selectedVal={this.onClickVal} setSelectedVal={val => this.handleOptionChange(val as string)} - title={'Choose onClick behaviour'} + title="Choose onClick behaviour" color={SettingsManager.userColor} dropdownType={DropdownType.SELECT} type={Type.SEC} @@ -440,16 +398,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { docView.toggleFollowLink(false, false); docView.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined; break; + default: } }); }; - @undoBatch - editOnClickScript = () => { - if (SelectionManager.Views.length) SelectionManager.Views.forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick')); - else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, 'onClick'); - }; - @computed get onClickFlyout() { const buttonList = [ @@ -473,6 +426,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break; case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break; case 'nothing': active = !linkButton && this.selectedDoc.onClick === undefined;break; + default: } return ( <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}> @@ -494,6 +448,36 @@ export class PropertiesButtons extends React.Component<{}, {}> { </div> ); } + @undoBatch + editOnClickScript = () => { + if (SelectionManager.Views.length) SelectionManager.Views.forEach(dv => DocUtils.makeCustomViewClicked(dv.Document, undefined, 'onClick')); + else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, 'onClick'); + }; + + propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { + const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedLayoutDoc; + const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => { + (dv?.layoutDoc || doc)[prop] = !(dv?.layoutDoc || doc)[prop]; + }; + return !targetDoc ? null : ( + <Toggle + toggleStatus={BoolCast(targetDoc[property])} + tooltip={tooltip(BoolCast(targetDoc[property]))} + text={label(targetDoc?.[property])} + color={SettingsManager.userColor} + icon={icon(targetDoc?.[property] as any)} + iconPlacement="left" + align="flex-start" + fillWidth + toggleType={ToggleType.BUTTON} + onClick={undoable(() => { + if (SelectionManager.Views.length > 1) { + SelectionManager.Views.forEach(dv => (onClick ?? onPropToggle)(dv, dv.Document, property)); + } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); + }, property)} + /> + ); + }; render() { const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)]; @@ -505,11 +489,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isStacking = [CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.NoteTaking].includes(this.selectedDoc?._type_collection as any); const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform; const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree; - const isTabView = this.selectedTabView; const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => ( <div className="propertiesButtons-button" style={style}> - {' '} - {ele}{' '} + {ele} </div> ); const isNovice = Doc.noviceMode; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 2a847e51a..8ac0f7d7b 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -11,8 +11,8 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorResult, SketchPicker } from 'react-color'; -import * as Icons from 'react-icons/bs'; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs" -import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils'; +import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs" +import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { Doc, Field, FieldType, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc'; import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols'; @@ -45,6 +45,7 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { StyleProviderFuncType } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; + const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -218,7 +219,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps rows.push( <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}> - <EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} /> + <EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={returnEmptyString} SetValue={this.setKeyValue} /> </div> ); } @@ -264,13 +265,13 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable transform: Transform = Transform.Identity(); getTransform = () => this.transform; propertiesDocViewRef = (ref: HTMLDivElement) => { - const observer = new _global.ResizeObserver( + const resizeObserver = new _global.ResizeObserver( action(() => { const cliRect = ref.getBoundingClientRect(); this.transform = new Transform(-cliRect.x, -cliRect.y, 1); }) ); - ref && observer.observe(ref); + ref && resizeObserver.observe(ref); }; @computed get contexts() { @@ -359,9 +360,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps if (permission === '-multiple-') dropdownValues.unshift(permission); return ( <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {dropdownValues.map(permission => ( - <option className="propertiesView-permisssions-select" key={permission} value={permission}> - {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + {dropdownValues.map(permissionVal => ( + <option className="propertiesView-permisssions-select" key={permissionVal} value={permissionVal}> + {concat(ReverseHierarchyMap.get(permissionVal)?.image, ' ', permissionVal)} </option> ))} </select> @@ -404,10 +405,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps /** * @returns a row of the permissions panel */ - sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { - if (name === ClientUtils.CurrentUserEmail()) { - name = 'Me'; - } + sharingItem(nameIn: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { + const name = nameIn === ClientUtils.CurrentUserEmail() ? 'Me' : nameIn; return ( <div className="propertiesView-sharingTable-item" @@ -466,7 +465,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const docs = SelectionManager.Views.length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views.map(docView => docView.Document); const target = docs[0]; - const showAdmin = GetEffectiveAcl(target) == AclAdmin; + const showAdmin = GetEffectiveAcl(target) === AclAdmin; const individualTableEntries = []; const usersAdded: string[] = []; // all shared users being added - organized by denormalized email @@ -841,7 +840,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps colorButton(value: string, type: string, setter: () => void) { return ( - <div className="color-button" key="color" onPointerDown={action(e => setter())}> + <div className="color-button" key="color" onPointerDown={action(() => setter())}> <div className="color-button-preview" style={{ @@ -858,9 +857,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps colorPicker(color: string, setter: (color: string) => void) { return ( - // prettier-ignore <SketchPicker - onChange={undoable( action((color: ColorResult) => setter(color.hex)), 'set stroke color property' )} + onChange={undoable( + action((col: ColorResult) => setter(col.hex)), + 'set stroke color property' + )} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} color={color} /> @@ -928,13 +929,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value); } - @computed get stkInput() { - return this.regInput('stk', this.widthStk, (val: string) => { this.widthStk = val; }); // prettier-ignore - } - @computed get markScaleInput() { - return this.regInput('scale', this.markScal.toString(), (val: string) => { this.markScal = Number(val); }); // prettier-ignore - } - regInput = (key: string, value: any, setter: (val: string) => {}) => ( <div className="inputBox"> <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} /> @@ -1640,7 +1634,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> <p>Zoom %</p> <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> - <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} type="number" value={zoom} /> + <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly type="number" value={zoom} /> <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> <FontAwesomeIcon icon="caret-up" /> @@ -1766,7 +1760,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps color: SettingsManager.userColor, backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Transitions + <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> Transitions <div className="propertiesView-presentationTrails-title-icon"> <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" /> </div> diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index db3f29fae..6195dcde8 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -197,7 +199,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr </div> ); }; - const renderMeta = (tag: string, dflt: FieldResult<FieldType>) => { + const renderMeta = (tag: string) => { const active = this.childFilters().includes(`${tag}${Doc.FilterSep}${Doc.FilterAny}${Doc.FilterSep}exists`); return ( <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this._props.Document, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}> @@ -228,13 +230,12 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null} {this.allHashtags.map(renderTag)} - {Array.from(this.allMetadata.keys()) - .sort() - .map(key => renderMeta(key, this.allMetadata.get(key)))} + {Array.from(this.allMetadata.keys()).sort().map(renderMeta)} </div> <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> <CollectionStackingView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} @@ -248,11 +249,11 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr isAnnotationOverlay={false} select={emptyFunction} NativeDimScaling={returnOne} - //childlayout_showTitle={this.layout_showTitle} + // childlayout_showTitle={this.layout_showTitle} isAnyChildContentActive={returnFalse} childDocumentsActive={this._props.isContentActive} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} - childHideDecorationTitle={true} + childHideDecorationTitle removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index d8adfa68a..75f1a7d80 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; @@ -74,11 +77,10 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) { const style: { [key: string]: any } = {}; const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; - const replacer = (match: any, expr: string, offset: any, string: any) => { + const replacer = (match: any, expr: string) => // bcz: this executes a script to convert a property expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? ''; - }; - divKeys.map((prop: string) => { + ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ this: doc, self: doc, scale }).result?.toString() ?? ''; + divKeys.forEach((prop: string) => { const p = (props as any)[prop]; typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); }); @@ -105,20 +107,18 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & const isNonTransparentLevel = isNonTransparent ? Number(property.replace(/.*:nonTransparent([0-9]+).*/, '$1')) : 0; // property.includes(':nonTransparent'); const isAnnotated = property.includes(':annotated'); const layoutDoc = doc ? Doc.Layout(doc) : doc; - const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString; const isOpen = property.includes(':open'); - const isEmpty = property.includes(':empty'); const boxBackground = property.includes(':box'); const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : ''; + const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString; const lockedPosition = () => doc && BoolCast(doc._lockedPosition); const titleHeight = () => props?.styleProvider?.(doc, props, StyleProp.TitleHeight); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor + ':nonTransparent' + (isNonTransparentLevel + 1)); - const color = () => props?.styleProvider?.(doc, props, StyleProp.Color); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); - const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); + const layoutShowTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); // prettier-ignore switch (property.split(':')[0]) { - case StyleProp.TreeViewIcon: + case StyleProp.TreeViewIcon: { const img = ImageCast(doc?.icon, ImageCast(doc?.data)); if (img) { const ext = extname(img.url.href); @@ -126,13 +126,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & return <img src={url} width={20} height={15} style={{ margin: 'auto', display: 'block', objectFit: 'contain' }} />; } return Doc.toIcon(doc, isOpen); - case StyleProp.TreeViewSortings: + } + case StyleProp.TreeViewSortings: { const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {}; allSorts[TreeSort.AlphaDown] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> }; allSorts[TreeSort.AlphaUp] = { color: 'crimson', icon: <BsArrowUp/> }; if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' }; allSorts[TreeSort.WhenAdded] = { color: 'darkgray', icon: <BsArrowDownUp/> }; return allSorts; + } case StyleProp.Highlighting: if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined; if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) { @@ -178,7 +180,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & )) || '' ); - case StyleProp.Color: + case StyleProp.Color: { if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userBackgroundColor; if (Doc.IsSystem(doc!)) return SettingsManager.userColor; if (doc?.type === DocumentType.FONTICON) return SettingsManager.userColor; @@ -186,30 +188,33 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & if (docColor) return docColor; const backColor = backgroundCol(); return backColor ? lightOrDark(backColor) : undefined; - case StyleProp.BorderRounding: + } + case StyleProp.BorderRounding: { const rounding = StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : '')); return (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc || doc?.isTemplateDoc) ? StrCast(doc._layout_borderRounding,rounding) : rounding; - case StyleProp.BorderPath: + } + case StyleProp.BorderPath: { const borderPath = Doc.IsComicStyle(doc) && props?.renderDepth && !doc?.layout_isSvg && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }; - return !borderPath - ? null - : { - clipPath: `path('${borderPath.path}')`, - jsx: ( - <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> - <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}> - <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> - </svg> - </div> - ), - }; + return !borderPath + ? null + : { + clipPath: `path('${borderPath.path}')`, + jsx: ( + <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> + <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}> + <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> + </svg> + </div> + ), + }; + } case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) || - (doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) || + (doc?.type === DocumentType.RTF && !layoutShowTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) && - layout_showTitle() && + layoutShowTitle() && !StrCast(doc?.layout_showTitle).includes(':hover') ? titleHeight() : 0; @@ -248,7 +253,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & (Colors.DARK_GRAY) : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY)); break; - //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; + // if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; default: docColor = docColor || (Colors.WHITE); } if (isNonTransparent && isNonTransparentLevel < 9 && (!docColor || docColor === 'transparent') && doc?.embedContainer && props?.styleProvider) { @@ -273,6 +278,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & case DocumentType.LABEL: if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px'; + // eslint-disable-next-line no-fallthrough default: return doc.z ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow @@ -299,7 +305,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & isGroup )? undefined: 'all' if (props?.isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all'; return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active - case StyleProp.Decorations: + case StyleProp.Decorations: { const lock = () => doc?.pointerEvents !== 'none' ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> <FontAwesomeIcon icon='lock' size="lg" /> @@ -316,16 +322,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length ? 'green' // #18c718bd' //'hasFilter' : props?.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || props?.childFiltersByRanges().length - ? 'orange' //'inheritsFilter' + ? 'orange' // 'inheritsFilter' : undefined; return !showFilterIcon ? null : ( <div className="styleProvider-filter"> <Dropdown type={Type.TERT} dropdownType={DropdownType.CLICK} - fillWidth - iconProvider={(active:boolean) => <div className='styleProvider-filterShift'><FaFilter/></div>} - closeOnSelect={true} + fillWidth + // eslint-disable-next-line react/no-unstable-nested-components + iconProvider={() => <div className='styleProvider-filterShift'><FaFilter/></div>} + closeOnSelect setSelectedVal={ action((dv) => { (dv as any).select(false); @@ -378,6 +385,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & {audio()} </> ); + } + default: } } diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx index 068143225..2d461c0ab 100644 --- a/src/client/views/UndoStack.tsx +++ b/src/client/views/UndoStack.tsx @@ -1,6 +1,7 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { Tooltip } from '@mui/material'; import { Popup, Type } from 'browndash-components'; -import { observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { StrCast } from '../../fields/Types'; @@ -15,19 +16,19 @@ export class UndoStack extends React.Component<UndoStackProps> { const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor; const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor; return ( - <Tooltip title={'undo stack (if it stays yellow, undo is broken - you should reload Dash)'}> + <Tooltip title="undo stack (if it stays yellow, undo is broken - you should reload Dash)"> <div> <div className="undoStack-outerContainer"> <Popup text="stack" color={color} background={background} - placement={`top-start`} + placement="top-start" type={Type.TERT} popup={ <div className="undoStack-commandsContainer" - ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} + ref={r => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 })} style={{ background, color, @@ -35,12 +36,13 @@ export class UndoStack extends React.Component<UndoStackProps> { {Array.from(UndoManager.undoStackNames).map((name, i) => ( <div className="undoStack-resultContainer" + // eslint-disable-next-line react/no-array-index-key key={i} - onClick={e => { + onClick={() => { const size = UndoManager.undoStackNames.length; for (let n = 0; n < size - i; n++) UndoManager.Undo(); }}> - <div className="undoStack-commandString">{StrCast(name).replace(/[^\.]*\./, '')}</div> + <div className="undoStack-commandString">{StrCast(name).replace(/[^.]*\./, '')}</div> </div> ))} {Array.from(UndoManager.redoStackNames) @@ -48,12 +50,13 @@ export class UndoStack extends React.Component<UndoStackProps> { .map((name, i) => ( <div className="undoStack-resultContainer" + // eslint-disable-next-line react/no-array-index-key key={i} - onClick={e => { + onClick={() => { for (let n = 0; n <= i; n++) UndoManager.Redo(); }}> <div className="undoStack-commandString" style={{ fontWeight: 'bold', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> - {StrCast(name).replace(/[^\.]*\./, '')} + {StrCast(name).replace(/[^.]*\./, '')} </div> </div> ))} diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index c3e513154..f03e316a2 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -9,6 +9,7 @@ import { Utils, emptyFunction } from '../../../Utils'; import { Doc, DocListCast } from '../../../fields/Doc'; import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; +// eslint-disable-next-line import/extensions import clamp from '../../util/clamp'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { FieldViewProps } from '../nodes/FieldView'; diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 490a14be5..1e4ed74be 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -73,7 +73,7 @@ export class Track extends ObservableReactComponent<IProps> { this._timelineVisibleReaction?.(); this._autoKfReaction?.(); } - //////////////////////////////// + // ////////////////////////////// getLastRegionTime = () => { let lastTime: number = 0; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 2d906dfe7..8803f6f79 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -1,10 +1,13 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction, numberRange } from '../../../Utils'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; @@ -52,13 +55,19 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF @observable private collapsed: boolean = false; @observable private _paletteOn = false; private set _heading(value: string) { - runInAction(() => this._props.headingObject && (this._props.headingObject.heading = this.heading = value)); + runInAction(() => { + this._props.headingObject && (this._props.headingObject.heading = this.heading = value); + }); } private set _color(value: string) { - runInAction(() => this._props.headingObject && (this._props.headingObject.color = this.color = value)); + runInAction(() => { + this._props.headingObject && (this._props.headingObject.color = this.color = value); + }); } private set _collapsed(value: boolean) { - runInAction(() => this._props.headingObject && (this._props.headingObject.collapsed = this.collapsed = value)); + runInAction(() => { + this._props.headingObject && (this._props.headingObject.collapsed = this.collapsed = value); + }); } private _dropDisposer?: DragManager.DragDropDisposer; @@ -88,7 +97,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF if (this.collapsed) { this._props.setDocHeight(this.heading, 20); } else { - const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header + const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; // +15 accounts for the group header const transformScale = this._props.screenToLocalTransform().Scale; const trueHeight = rawHeight * transformScale; this._props.setDocHeight(this.heading, trueHeight); @@ -102,7 +111,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF const key = this._props.pivotField; const castedValue = this.getValue(this.heading); if (this._props.parent.onInternalDrop(e, de)) { - key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !this.onLayoutDoc(key))); + key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); } return true; } @@ -118,7 +127,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF }; @action - headingChanged = (value: string, shiftDown?: boolean) => { + headingChanged = (value: string /* , shiftDown?: boolean */) => { this._createEmbeddingSelected = false; const key = this._props.pivotField; const castedValue = this.getValue(value); @@ -141,7 +150,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF this._color = color; }; - pointerEnteredRow = action(() => SnappingManager.IsDragging && (this._background = '#b4b4b4')); + pointerEnteredRow = action(() => { + SnappingManager.IsDragging && (this._background = '#b4b4b4'); + }); @action pointerLeaveRow = () => { @@ -153,13 +164,13 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; this._createEmbeddingSelected = false; - const key = this._props.pivotField; + const { pivotField } = this._props; const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value }); FormattedTextBox.SetSelectOnLoad(newDoc); FormattedTextBox.SelectOnLoadChar = value; - key && ((this.onLayoutDoc(key) ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading)); + pivotField && (newDoc[DocData][pivotField] = this.getValue(this._props.heading)); const docs = this._props.parent.childDocList; - return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) + return docs ? !!docs.splice(0, 0, newDoc) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) }; deleteRow = undoBatch( @@ -198,21 +209,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { - setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this._props.chromeHidden && this.collapseSection(e)); + setupMoveUpEvents(this, e, this.headerMove, emptyFunction, clickEv => !this._props.chromeHidden && this.collapseSection(clickEv)); this._createEmbeddingSelected = false; } }; - /** - * Returns true if a key is on the layout doc of the documents in the collection. - */ - onLayoutDoc = (key: string): boolean => { - DocListCast(this._props.parent.Document.data).forEach(doc => { - if (Doc.Get(doc, key, true)) return true; - }); - return false; - }; - renderColorPicker = () => { const selected = this.color; @@ -229,27 +230,29 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF return ( <div className="collectionStackingView-colorPicker"> <div className="colorOptions"> - <div className={'colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> - <div className={'colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> - <div className={'colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> - <div className={'colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> - <div className={'colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> - <div className={'colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> - <div className={'colorPicker' + (selected === green ? ' active' : '')} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div> - <div className={'colorPicker' + (selected === cyan ? ' active' : '')} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div> - <div className={'colorPicker' + (selected === orange ? ' active' : '')} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div> + <div className={'colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)} /> + <div className={'colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)} /> + <div className={'colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)} /> + <div className={'colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)} /> + <div className={'colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)} /> + <div className={'colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)} /> + <div className={'colorPicker' + (selected === green ? ' active' : '')} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)} /> + <div className={'colorPicker' + (selected === cyan ? ' active' : '')} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)} /> + <div className={'colorPicker' + (selected === orange ? ' active' : '')} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)} /> </div> </div> ); }; - toggleEmbedding = action(() => (this._createEmbeddingSelected = true)); - toggleVisibility = () => (this._collapsed = !this.collapsed); + toggleEmbedding = action(() => { + this._createEmbeddingSelected = true; + }); + toggleVisibility = () => { + this._collapsed = !this.collapsed; + }; @action - textCallback = (char: string) => { - return this.addDocument('', false); - }; + textCallback = (/* char: string */) => this.addDocument('', false); @computed get contentLayout() { const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap)))); @@ -263,22 +266,22 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF className="collectionStackingView-addDocumentButton" style={ { - //width: style.columnWidth / style.numGroupColumns, - //padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`, + // width: style.columnWidth / style.numGroupColumns, + // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`, } }> - <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents={'+ NEW'} /> + <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" /> </div> ) : null} <div - className={`collectionStackingView-masonryGrid`} + className="collectionStackingView-masonryGrid" ref={this._contRef} style={{ padding: stackPad, minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined, width: this._props.parent.NodeWidth, gridGap: this._props.parent.gridGap, - gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this._props.parent.columnWidth}px`, ''), + gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''), }}> {this._props.parent.children(this._props.docList)} </div> @@ -290,7 +293,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF const noChrome = this._props.chromeHidden; const key = this._props.pivotField; const evContents = this.heading ? this.heading : this._props.type && this._props.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`; - const editableHeaderView = <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />; + const editableHeaderView = <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine />; return this._props.Document.miniHeaders ? ( <div className="collectionStackingView-miniHeader">{editableHeaderView}</div> ) : !this._props.headingObject ? null : ( @@ -304,6 +307,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionColor"> <button + type="button" className="collectionStackingView-sectionColorButton" onPointerDown={e => setupMoveUpEvents( @@ -311,7 +315,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF e, returnFalse, emptyFunction, - action(e => (this._paletteOn = !this._paletteOn)) + action(() => { + this._paletteOn = !this._paletteOn; + }) ) }> <FontAwesomeIcon icon="palette" size="lg" /> @@ -320,13 +326,13 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF </div> )} {noChrome ? null : ( - <button className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}> + <button type="button" className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}> <FontAwesomeIcon icon={this.collapsed ? 'chevron-down' : 'chevron-up'} size="lg" /> </button> )} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionOptions" onPointerDown={e => e.stopPropagation()}> - <button className="collectionStackingView-sectionOptionButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}> + <button type="button" className="collectionStackingView-sectionOptionButton" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}> <FontAwesomeIcon icon="trash" size="lg" /> </button> </div> diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index a23725348..5a509128d 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -36,6 +36,7 @@ interface CollectionMenuProps { @observer export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { + // eslint-disable-next-line no-use-before-define @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined = undefined; @@ -63,7 +64,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { } @action - toggleMenuPin = (e: React.MouseEvent) => { + toggleMenuPin = () => { Doc.UserDoc()['menuCollections-pinned'] = this.Pinned = !this.Pinned; if (!this.Pinned && this._left < 0) { this.jumpTo(300, 300); @@ -165,7 +166,9 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { } interface CollectionViewMenuProps { + // eslint-disable-next-line react/no-unused-prop-types type: CollectionViewType; + // eslint-disable-next-line react/no-unused-prop-types fieldKey: string; docView: DocumentView; } @@ -377,16 +380,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu setupMoveUpEvents( this, e, - (e, down, delta) => { + moveEv => { const vtype = this.props.type; const c = { params: ['target'], title: vtype, script: `this.target._type_collection = '${StrCast(this.props.type)}'`, - immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])), + immediate: (source: Doc[]) => { + this.document._type_collection = Doc.getDocTemplate(source?.[0]); + }, initialize: emptyFunction, }; - DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY); + DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY); return true; }, emptyFunction, @@ -397,8 +402,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu setupMoveUpEvents( this, e, - (e, down, delta) => { - this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, e.clientX, e.clientY)); + moveEv => { + this._buttonizableCommands + ?.filter(c => c.title === this._currentKey) + .map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY)); return true; }, emptyFunction, diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 4ceeb631d..2ba6f5bf4 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -59,7 +59,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } @computed get chromeHidden() { - return BoolCast(this.layoutDoc.chromeHidden) || this._props.onBrowseClickScript?.() ? true : false; + return !!(BoolCast(this.layoutDoc.chromeHidden) || this._props.onBrowseClickScript?.()); } // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get colHeaderData() { @@ -68,7 +68,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) { setTimeout(() => { const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []); - const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); if (needsUnsetCategory || columnHeaders.length === 0) { columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); this.resizeColumns(columnHeaders); @@ -112,12 +111,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { // to render the docs you see within an individual column. children = (docs: Doc[]) => { TraceMobx(); - return docs.map((d, i) => { + return docs.map(d => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); const style = { width: width(), marginTop: this.gridGap, height: height() }; return ( - <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}> + <div className="collectionNoteTakingView-columnDoc" key={d[Id]} style={style}> {this.getDisplayDoc(d, width)} </div> ); @@ -136,7 +135,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); const rowCol = this.docsDraggedRowCol; // this will sort the docs into the correct columns (minus the ones you're currently dragging) - docs.map(d => { + docs.forEach(d => { const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; // look for if header exists already const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); @@ -154,7 +153,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { removeDocDragHighlight = () => { setTimeout( - action(() => (this.docsDraggedRowCol.length = 0)), + action(() => { + this.docsDraggedRowCol.length = 0; + }), 100 ); }; @@ -192,13 +193,11 @@ export class CollectionNoteTakingView extends CollectionSubView() { Object.keys(this._disposers).forEach(key => this._disposers[key]()); } - moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => { - return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; - }; + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDashEventsTarget(ele!); //so the whole grid is the drop target? + this.createDashEventsTarget(ele!); }; @computed get onChildClickHandler() { @@ -218,14 +217,15 @@ export class CollectionNoteTakingView extends CollectionSubView() { Doc.BrushDoc(doc); const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); if (found) { - const top = found.getBoundingClientRect().top; + const { top } = found.getBoundingClientRect(); const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0 && Math.ceil(this._props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) { - let focusSpeed = options.zoomTime ?? 500; + const focusSpeed = options.zoomTime ?? 500; smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); return focusSpeed; } } + return undefined; }; styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { @@ -240,6 +240,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { return this._props.childOpacity(); } break; + default: } return this._props.styleProvider?.(doc, props, property); }; @@ -255,7 +256,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { const noteTakingDocTransform = () => this.getDocTransform(doc, dref); return ( <DocumentView - ref={r => (dref = r || undefined)} + ref={r => { + dref = r || undefined; + }} Document={doc} TemplateDataDocument={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} pointerEvents={this.blockPointerEventsWhenDragging} @@ -267,8 +270,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { layout_fitWidth={this._props.childLayoutFitWidth} isContentActive={emptyFunction} onKey={this.onKeyDown} - //TODO: change this from a prop to a parameter passed into a function - dontHideOnDrag={true} + // TODO: change this from a prop to a parameter passed into a function + dontHideOnDrag isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} @@ -302,7 +305,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns getDocTransform(doc: Doc, dref?: DocumentView) { - const y = this._scroll; // required for document decorations to update when the text box container is scrolled + this._scroll; // required for document decorations to update when the text box container is scrolled const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv || undefined); // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale); @@ -347,7 +350,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33] @action resizeColumns = (headers: SchemaHeaderField[]) => { - const n = headers.length; const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0); const scaleFactor = 1 / curWidths; this.dataDoc[this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>( @@ -385,7 +387,11 @@ export class CollectionNoteTakingView extends CollectionSubView() { // we alter the pivot fields of the docs in case they are moved to a new column. const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); - DragManager.docsBeingDragged.map(doc => doc[DocData]).forEach(d => (d[this.notetakingCategoryField] = colHeader)); + DragManager.docsBeingDragged + .map(doc => doc[DocData]) + .forEach(d => { + d[this.notetakingCategoryField] = colHeader; + }); // used to notify sections to re-render this.docsDraggedRowCol.length = 0; const columnFromCoord = this.getColumnFromXCoord(xCoord); @@ -396,7 +402,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate). // This function is used to know which document a column SHOULD be in while it is being dragged. getColumnFromXCoord = (xCoord: number): number | undefined => { - let colIndex: number | undefined = undefined; + let colIndex: number | undefined; const numColumns = this.colHeaderData.length; const coords = []; let colStartXCoord = 0; @@ -419,10 +425,10 @@ export class CollectionNoteTakingView extends CollectionSubView() { const docsMatchingHeader: Doc[] = []; const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); - this.childDocs?.map(d => { + this.childDocs?.forEach(d => { if (d instanceof Promise) return; const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; - if (sectionValue.toString() == colHeader) { + if (sectionValue.toString() === colHeader) { docsMatchingHeader.push(d); } }); @@ -438,6 +444,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { FormattedTextBox.SetSelectOnLoad(newDoc); return this.addDocument?.(newDoc); } + return undefined; }; // onInternalDrop is used when dragging and dropping a document within the view, such as dragging @@ -575,6 +582,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { alert('You cannot use an existing column name. Please try a new column name'); return value; } + return undefined; }); const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); const newColWidth = 1 / (this.numGroupColumns + 1); @@ -593,17 +601,33 @@ export class CollectionNoteTakingView extends CollectionSubView() { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`, - event: () => (this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate), + event: () => { + this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate; + }, icon: 'computer', }); subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' }); subItems.push({ description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`, - event: () => (this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize), + event: () => { + this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize; + }, icon: 'plus', }); - subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' }); - subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' }); + subItems.push({ + description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, + event: () => { + this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; + }, + icon: 'plus', + }); + subItems.push({ + description: 'Clear All', + event: () => { + this.dataDoc.data = new List([]); + }, + icon: 'times', + }); ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); } }; @@ -628,6 +652,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const sections = Array.from(this.Sections.entries()); return sections.reduce((list, sec, i) => { list.push(this.sectionNoteTaking(sec[0], sec[1])); + // eslint-disable-next-line react/no-array-index-key i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); return list; }, [] as JSX.Element[]); @@ -661,14 +686,18 @@ export class CollectionNoteTakingView extends CollectionSubView() { background: this.backgroundColor(), pointerEvents: this.backgroundEvents, }} - onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} - onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} + onScroll={action(e => { + this._scroll = e.currentTarget.scrollTop; + })} + onPointerLeave={action(() => { + this.docsDraggedRowCol.length = 0; + })} onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this._props.isContentActive() && e.stopPropagation()}> - <>{this.renderedSections}</> + {this.renderedSections} <div className="collectionNotetaking-pivotField" style={{ right: 0, top: 0, position: 'absolute' }}> <FieldsDropdown Document={this.Document} diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index e39f1c700..6dfc38e2e 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -67,6 +69,7 @@ export class CollectionPileView extends CollectionSubView() { return ( <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} // layoutEngine={this.layoutEngine} addDocument={this.addPileDoc} @@ -117,16 +120,16 @@ export class CollectionPileView extends CollectionSubView() { setupMoveUpEvents( this, e, - (e: PointerEvent, down: number[], delta: number[]) => { - if (this.layoutEngine() === 'pass' && this.childDocs.length && e.shiftKey) { + (moveEv: PointerEvent, down: number[], delta: number[]) => { + if (this.layoutEngine() === 'pass' && this.childDocs.length && moveEv.shiftKey) { dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]); if (dist > 100) { if (!this._undoBatch) { this._undoBatch = UndoManager.StartBatch('layout pile'); } const doc = this.childDocs[0]; - doc.x = e.clientX; - doc.y = e.clientY; + doc.x = moveEv.clientX; + doc.y = moveEv.clientY; this._props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this._props.removeDocument?.(doc) || false); dist = 0; } @@ -155,7 +158,7 @@ export class CollectionPileView extends CollectionSubView() { render() { return ( - <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}> + <div className="collectionPileView" onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}> {this.contents} </div> ); diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 3f6638b25..7adf44a5c 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,3 +1,8 @@ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable no-use-before-define */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -22,7 +27,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; -import { CollectionSubView } from '../collections/CollectionSubView'; +import { CollectionSubView } from './CollectionSubView'; import { LightboxView } from '../LightboxView'; import { AudioWaveform } from '../nodes/audio/AudioWaveform'; import { DocumentView, OpenWhere } from '../nodes/DocumentView'; @@ -58,9 +63,14 @@ export enum TrimScope { @observer export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() { + // eslint-disable-next-line no-use-before-define public static SelectingRegions: Set<CollectionStackedTimeline> = new Set(); public static StopSelecting() { - this.SelectingRegions.forEach(action(region => (region._selectingRegion = false))); + this.SelectingRegions.forEach( + action(region => { + region._selectingRegion = false; + }) + ); this.SelectingRegions.clear(); } constructor(props: any) { @@ -131,7 +141,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack componentWillUnmount() { document.removeEventListener('keydown', this.keyEvents, true); if (this._selectingRegion) { - runInAction(() => (this._selectingRegion = false)); + runInAction(() => { + this._selectingRegion = false; + }); CollectionStackedTimeline.SelectingRegions.delete(this); } } @@ -175,9 +187,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this._props.endTag], val) ?? null); // converts screen pixel offset to time - toTimeline = (screen_delta: number, width: number) => { - return Math.max(this.clipStart, Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart)); - }; + // prettier-ignore + toTimeline = (screenDelta: number, width: number) => // + Math.max(this.clipStart, Math.min(this.clipEnd, (screenDelta / width) * this.clipDuration + this.clipStart)); @computed get rangeClick() { // prettier-ignore @@ -235,6 +247,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._props.setTime(Math.min(Math.max(this.clipStart, this.currentTime + jump), this.clipEnd)); e.stopPropagation(); break; + default: } } }; @@ -254,17 +267,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @action onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); - const clientX = e.clientX; - const diff = rect ? clientX - rect?.x : null; - const shiftKey = e.shiftKey; + const { clientX, shiftKey } = e; if (rect && this._props.isContentActive()) { const wasPlaying = this._props.playing(); if (wasPlaying) this._props.Pause(); - var wasSelecting = this._markerEnd !== undefined; + let wasSelecting = this._markerEnd !== undefined; setupMoveUpEvents( this, e, - action(e => { + action(() => { if (!wasSelecting) { this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); wasSelecting = true; @@ -273,8 +284,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); return false; }), - action((e, movement, isClick) => { - this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); + action((upEvent, movement, isClick) => { + this._markerEnd = this.toTimeline(upEvent.clientX - rect.x, rect.width); if (this._markerEnd < this._markerStart) { const tmp = this._markerStart; this._markerStart = this._markerEnd; @@ -287,8 +298,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack (!isClick || !wasSelecting) && (this._markerEnd = undefined); this._timelineWrapper && (this._timelineWrapper.style.cursor = ''); }), - (e, doubleTap) => { - if (e.button !== 2) { + (clickEv, doubleTap) => { + if (clickEv.button !== 2) { this._props.select(false); !wasPlaying && doubleTap && this._props.Play(); } @@ -310,7 +321,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack onHover = (e: React.MouseEvent): void => { e.stopPropagation(); const rect = this._timeline?.getBoundingClientRect(); - const clientX = e.clientX; + const { clientX } = e; if (rect) { this._hoverTime = this.toTimeline(clientX - rect.x, rect.width); if (this.thumbnails) { @@ -328,14 +339,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack setupMoveUpEvents( this, e, - action((e, [], []) => { + action(moveEv => { if (rect && this._props.isContentActive()) { - this._trimStart = Math.min(Math.max(this.trimStart + (e.movementX / rect.width) * this.clipDuration, this.clipStart), this.trimEnd - this.minTrimLength); + this._trimStart = Math.min(Math.max(this.trimStart + (moveEv.movementX / rect.width) * this.clipDuration, this.clipStart), this.trimEnd - this.minTrimLength); } return false; }), emptyFunction, - action((e, doubleTap) => { + action((clickEv, doubleTap) => { doubleTap && (this._trimStart = this.clipStart); }) ); @@ -348,14 +359,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack setupMoveUpEvents( this, e, - action((e, [], []) => { + action(moveEv => { if (rect && this._props.isContentActive()) { - this._trimEnd = Math.max(Math.min(this.trimEnd + (e.movementX / rect.width) * this.clipDuration, this.clipEnd), this.trimStart + this.minTrimLength); + this._trimEnd = Math.max(Math.min(this.trimEnd + (moveEv.movementX / rect.width) * this.clipDuration, this.clipEnd), this.trimStart + this.minTrimLength); } return false; }), emptyFunction, - action((e, doubleTap) => { + action((clickEv, doubleTap) => { doubleTap && (this._trimEnd = this.clipEnd); }) ); @@ -384,7 +395,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack // handles dragging and dropping markers in timeline @action - internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) { if (super.onInternalDrop(e, de)) { // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view const localPt = this.ScreenToLocalBoxXf().transformPoint(de.x, de.y); @@ -404,7 +415,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack } onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0); + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; @@ -442,7 +453,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack } @action - playOnClick = (anchorDoc: Doc, clientX: number) => { + playOnClick = (anchorDoc: Doc /* , clientX: number */) => { const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlayAnchors) { @@ -451,17 +462,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._props.playFrom(seekTimeInSeconds, endTime); this.scrollToTime(seekTimeInSeconds); } - } else { - if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) { - if (!this.layoutDoc.autoPlayAnchors && this._props.playing()) { - this._props.Pause(); - } else { - this._props.Play(); - } + } else if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) { + if (!this.layoutDoc.autoPlayAnchors && this._props.playing()) { + this._props.Pause(); } else { - this._props.playFrom(seekTimeInSeconds, endTime); - this.scrollToTime(seekTimeInSeconds); + this._props.Play(); } + } else { + this._props.playFrom(seekTimeInSeconds, endTime); + this.scrollToTime(seekTimeInSeconds); } return { select: true }; }; @@ -480,19 +489,17 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack const rect = this._timeline?.getBoundingClientRect(); rect && this._props.setTime(this.toTimeline(clientX - rect.x, rect.width)); } + } else if (this.layoutDoc.autoPlayAnchors) { + this._props.playFrom(seekTimeInSeconds, endTime); } else { - if (this.layoutDoc.autoPlayAnchors) { - this._props.playFrom(seekTimeInSeconds, endTime); - } else { - this._props.setTime(seekTimeInSeconds); - } + this._props.setTime(seekTimeInSeconds); } return { select: true }; }; // makes sure no anchors overlaps each other by setting the correct position and width getLevel = (m: Doc, placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]) => { - const timelineContentWidth = this.timelineContentWidth; + const { timelineContentWidth } = this; const x1 = this.anchorStart(m); const x2 = this.anchorEnd(m, x1 + (10 / timelineContentWidth) * this.clipDuration); let max = 0; @@ -504,6 +511,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack max = Math.max(max, p.level); return p.level; } + return undefined; }) ); let level = max + 1; @@ -565,10 +573,14 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack onWheel={e => this.isContentActive() && e.stopPropagation()} onScroll={this.setScroll} onMouseMove={e => this.isContentActive() && this.onHover(e)} - ref={wrapper => (this._timelineWrapper = wrapper)}> + ref={wrapper => { + this._timelineWrapper = wrapper; + }}> <div className="collectionStackedTimeline" - ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} + ref={(timeline: HTMLDivElement | null) => { + this._timeline = timeline; + }} onClick={e => this.isContentActive() && StopEvent(e)} onPointerDown={e => this.isContentActive() && this.onPointerDownTimeline(e)} style={{ width: this.timelineContentWidth }}> @@ -583,7 +595,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack const height = this._props.PanelHeight() / maxLevel; return this.Document.hideAnchors ? null : ( <div - className={'collectionStackedTimeline-marker-timeline'} + className="collectionStackedTimeline-marker-timeline" key={d.anchor[Id]} style={{ left, @@ -593,6 +605,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack pointerEvents: 'none', }}> <StackedTimelineAnchor + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} mark={d.anchor} containerViewPath={this._props.containerViewPath} @@ -647,7 +660,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack {this.IsTrimming !== TrimScope.None && ( <> - <div className="collectionStackedTimeline-trim-shade" style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }}></div> + <div className="collectionStackedTimeline-trim-shade" style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }} /> <div className="collectionStackedTimeline-trim-controls" @@ -655,8 +668,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack left: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%`, width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`, }}> - <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimLeft}></div> - <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimRight}></div> + <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimLeft} /> + <div className="collectionStackedTimeline-trim-handle" onPointerDown={this.trimRight} /> </div> <div @@ -664,7 +677,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack style={{ left: `${((this.trimEnd - this.clipStart) / this.clipDuration) * 100}%`, width: `${((this.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`, - }}></div> + }} + /> </> )} </div> @@ -686,7 +700,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack interface StackedTimelineAnchorProps { mark: Doc; whenChildContentsActiveChanged: (isActive: boolean) => void; - addDocTab: (doc: Doc, where: OpenWhere) => boolean; + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; rangeClickScript: () => ScriptField; rangePlayScript: () => ScriptField; left: number; @@ -736,13 +750,13 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch this._disposer = reaction( () => this._props.currentTimecode(), time => { - const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null); - const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc); + // const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null); + // const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc); if ( !LightboxView.LightboxDoc && // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. - /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc))*/ + /* (isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc)) */ !this._props.layoutDoc.dontAutoFollowLinks && LinkManager.Links(this._props.mark).length && time > NumCast(this._props.mark[this._props.startTag]) && @@ -764,34 +778,33 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch // starting the drag event for anchor resizing @action onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { - //this._props._timeline?.setPointerCapture(e.pointerId); - const newTime = (e: PointerEvent) => { - const rect = (e.target as any).getBoundingClientRect(); - return this._props.toTimeline(e.clientX - rect.x, rect.width); + const newTime = (timeDownEv: PointerEvent) => { + const rect = (timeDownEv.target as any).getBoundingClientRect(); + return this._props.toTimeline(timeDownEv.clientX - rect.x, rect.width); }; - const changeAnchor = (anchor: Doc, left: boolean, time: number | undefined) => { + const changeAnchor = (time: number | undefined) => { const timelineOnly = Cast(anchor[this._props.startTag], 'number', null) !== undefined; if (timelineOnly) { - if (!left && time !== undefined && time <= NumCast(anchor[this._props.startTag])) time = undefined; - Doc.SetInPlace(anchor, left ? this._props.startTag : this._props.endTag, time, true); - if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', time !== undefined ? undefined : '100%', true); + const timeMod = !left && time !== undefined && time <= NumCast(anchor[this._props.startTag]) ? undefined : time; + Doc.SetInPlace(anchor, left ? this._props.startTag : this._props.endTag, timeMod, true); + if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', timeMod !== undefined ? undefined : '100%', true); } else { anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time; } return false; }; this.noEvents = true; - var undo: UndoManager.Batch | undefined; + let undo: UndoManager.Batch | undefined; setupMoveUpEvents( this, e, - e => { + moveEv => { if (!undo) undo = UndoManager.StartBatch('drag anchor'); - this._props.setTime(newTime(e)); - return changeAnchor(anchor, left, newTime(e)); + this._props.setTime(newTime(moveEv)); + return changeAnchor(newTime(moveEv)); }, - action(e => { - this._props.setTime(newTime(e)); + action(upEv => { + this._props.setTime(newTime(upEv)); undo?.end(); this.noEvents = false; }), @@ -829,7 +842,9 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch {...this._props} NativeWidth={returnZero} NativeHeight={returnZero} - ref={action((r: DocumentView | null) => (anchor.view = r))} + ref={action((r: DocumentView | null) => { + anchor.view = r; + })} Document={mark} TemplateDataDocument={undefined} containerViewPath={this._props.containerViewPath} @@ -852,7 +867,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch onClickScript={script} onDoubleClickScript={this._props.layoutDoc.autoPlayAnchors ? undefined : doublescript} ignoreAutoHeight={false} - hideResizeHandles={true} + hideResizeHandles contextMenuItems={this.contextMenuItems} /> ), @@ -878,12 +893,15 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function formatToTime(time: number): any { return formatTime(time); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function min(num1: number, num2: number): number { return Math.min(num1, num2); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function max(num1: number, num2: number): number { return Math.max(num1, num2); }); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 7d93f4074..f3dedaedf 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -3,11 +3,12 @@ import * as React from 'react'; import * as rp from 'request-promise'; import { ClientUtils, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; -import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; import { AclPrivate } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; +import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; @@ -61,8 +62,8 @@ export interface SubCollectionViewProps extends CollectionViewProps { isAnyChildContentActive: () => boolean; } -export function CollectionSubView<X>(moreProps?: X) { - class CollectionSubView extends ViewBoxBaseComponent<X & SubCollectionViewProps>() { +export function CollectionSubView<X>() { + class CollectionSubViewInternal extends ViewBoxBaseComponent<X & SubCollectionViewProps>() { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _mainCont?: HTMLDivElement; @@ -167,23 +168,24 @@ export function CollectionSubView<X>(moreProps?: X) { if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0; const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes(CollectionView.name); - const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - const side = annos && d[fieldKey + '_sidebar']; - if (data !== undefined || side !== undefined) { - let subDocs = [...DocListCast(data), ...DocListCast(side)]; + const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); + const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey]; + const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar']; + if (docChildDocs !== undefined || sidebarDocs !== undefined) { + let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)]; if (subDocs.length > 0) { let newarray: Doc[] = []; notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; + // eslint-disable-next-line no-loop-func subDocs.forEach(t => { - const fieldKey = Doc.LayoutFieldKey(t); - const annos = !Field.toString(Doc.LayoutField(t) as FieldType).includes(CollectionView.name); + const docFieldKey = Doc.LayoutFieldKey(t); + const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc)); notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); - DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); - annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); + DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc)); + isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -227,6 +229,7 @@ export function CollectionSubView<X>(moreProps?: X) { } @undoBatch + // eslint-disable-next-line @typescript-eslint/no-unused-vars protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) { @@ -245,7 +248,7 @@ export function CollectionSubView<X>(moreProps?: X) { addDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.addDocument?.(doc, annotationKey) || false; removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.removeDocument?.(doc, annotationKey) || false; - moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false; + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; @@ -531,5 +534,5 @@ export function CollectionSubView<X>(moreProps?: X) { }; } - return CollectionSubView; + return CollectionSubViewInternal; } diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 7f234ffe9..e1d2e3c40 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -16,7 +18,6 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { FieldsDropdown } from '../FieldsDropdown'; import { DocumentView } from '../nodes/DocumentView'; -import { FocusViewOptions } from '../nodes/FieldView'; import { PresBox } from '../nodes/trails'; import { CollectionSubView } from './CollectionSubView'; import './CollectionTimeView.scss'; @@ -68,7 +69,7 @@ export class CollectionTimeView extends CollectionSubView() { }; @action - scrollPreview = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: FocusViewOptions) => { + scrollPreview = (docView: DocumentView, anchor: Doc /* , focusSpeed: number, options: FocusViewOptions */) => { // if in preview, then override document's fields with view spec this._focusFilters = StrListCast(anchor.config_docFilters); this._focusRangeFilters = StrListCast(anchor.config_docRangeFilters); @@ -77,13 +78,15 @@ export class CollectionTimeView extends CollectionSubView() { }; layoutEngine = () => this._layoutEngine; - toggleVisibility = action(() => (this._collapsed = !this._collapsed)); + toggleVisibility = action(() => { + this._collapsed = !this._collapsed; + }); onMinDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, - action((e: PointerEvent, down: number[], delta: number[]) => { + action((moveEv: PointerEvent, down: number[], delta: number[]) => { const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0)); const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10)); this.Document[this._props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this._props.PanelWidth(); @@ -99,7 +102,7 @@ export class CollectionTimeView extends CollectionSubView() { setupMoveUpEvents( this, e, - action((e: PointerEvent, down: number[], delta: number[]) => { + action((moveEv, down: number[], delta: number[]) => { const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0)); const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10)); this.Document[this._props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this._props.PanelWidth(); @@ -114,7 +117,7 @@ export class CollectionTimeView extends CollectionSubView() { setupMoveUpEvents( this, e, - action((e: PointerEvent, down: number[], delta: number[]) => { + action((moveEv: PointerEvent, down: number[], delta: number[]) => { const minReq = NumCast(this.Document[this._props.fieldKey + '-timelineMinReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMin'], 0)); const maxReq = NumCast(this.Document[this._props.fieldKey + '-timelineMaxReq'], NumCast(this.Document[this._props.fieldKey + '-timelineMax'], 10)); this.Document[this._props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this._props.PanelWidth(); @@ -134,7 +137,7 @@ export class CollectionTimeView extends CollectionSubView() { }; @action - contentsDown = (e: React.MouseEvent) => { + contentsDown = () => { const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex); if (prevFilterIndex > 0) { this.goTo(prevFilterIndex - 1); @@ -147,6 +150,7 @@ export class CollectionTimeView extends CollectionSubView() { return ( <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }} fitContentsToBox={returnTrue} @@ -162,7 +166,7 @@ export class CollectionTimeView extends CollectionSubView() { const fieldKey = Doc.LayoutFieldKey(doc); doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); } - specificMenu = (e: React.MouseEvent) => { + specificMenu = () => { const layoutItems: ContextMenuProps[] = []; const doc = this.layoutDoc; @@ -194,7 +198,7 @@ export class CollectionTimeView extends CollectionSubView() { render() { let nonNumbers = 0; - this.childDocs.map(doc => { + this.childDocs.forEach(doc => { const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField]))); if (isNaN(num)) { nonNumbers++; @@ -226,13 +230,20 @@ export class CollectionTimeView extends CollectionSubView() { </> )} <div style={{ right: 0, top: 0, position: 'absolute' }}> - <FieldsDropdown Document={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} /> + <FieldsDropdown + Document={this.Document} + selectFunc={fieldKey => { + this.layoutDoc._pivotField = fieldKey; + }} + placeholder={StrCast(this.layoutDoc._pivotField)} + /> </div> </div> ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { const pivotField = StrCast(pivotDoc._pivotField, 'author'); let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); @@ -249,6 +260,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { + // eslint-disable-next-line prefer-destructuring pivotDoc._pivotField = filterVals[0]; } } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 32473f20b..538a6fd5e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -23,13 +25,13 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { DocumentView } from '../nodes/DocumentView'; -import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView } from './CollectionSubView'; import './CollectionTreeView.scss'; import { TreeView } from './TreeView'; + const _global = (window /* browser */ || global) /* node */ as any; export type collectionTreeViewProps = { @@ -94,8 +96,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; - whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); - isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this._props.isContentActive() ? true : false); + whenChildContentsActiveChanged = action((isActive: boolean) => { + this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); + }); + isContentActive = () => (this._isAnyChildContentActive ? true : !!this._props.isContentActive()); componentWillUnmount() { this._isDisposing = true; @@ -105,7 +109,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } componentDidMount() { - //this._props.setContentView?.(this); + // this._props.setContentView?.(this); this._disposers.autoheight = reaction( () => this.layoutDoc.layout_autoHeight, auto => auto && this.computeHeight(), @@ -128,20 +132,19 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree observeHeight = (ref: any) => { if (ref) { this.refList.add(ref); - this.observer = new _global.ResizeObserver( - action((entries: any) => { - if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) { - this.computeHeight(); - } - }) - ); + this.observer = new _global.ResizeObserver(() => { + if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) { + this.computeHeight(); + } + }); this.layoutDoc.layout_autoHeight && this.computeHeight(); this.observer.observe(ref); } }; protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); - if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this)); + this._mainEle = ele; + if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this)); }; protected onInternalDrop(e: Event, de: DragManager.DropEvent) { @@ -169,7 +172,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } }; - dragConfig = (dragData: DragManager.DocumentDragData) => (dragData.treeViewDoc = this.Document); + dragConfig = (dragData: DragManager.DocumentDragData) => { dragData.treeViewDoc = this.Document; }; // prettier-ignore screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, -this._headerHeight); @@ -198,10 +201,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @action addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { - const doclist = docs instanceof Doc ? [docs] : docs; - const addDocRelativeTo = (doc: Doc | Doc[]) => doclist.reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true); + const addDocRelativeTo = (adocs: Doc | Doc[]) => (adocs as Doc[]).reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true); if (this.Document.resolvedDataDoc instanceof Promise) return false; - const res = relativeTo === undefined ? this._props.addDocument?.(docs) || false : addDocRelativeTo(docs); + const doclist = docs instanceof Doc ? [docs] : docs; + const res = relativeTo === undefined ? this._props.addDocument?.(doclist) || false : addDocRelativeTo(doclist); res && doclist.forEach(doc => { Doc.SetContainer(doc, this.Document); @@ -209,7 +212,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree }); return res; }; - onContextMenu = (e: React.MouseEvent): void => { + onContextMenu = (): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout const layoutItems: ContextMenuProps[] = []; const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc; @@ -217,11 +220,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if (!Doc.noviceMode) { layoutItems.push({ description: 'Make tree state ' + (this.Document.treeView_OpenIsTransient ? 'persistent' : 'transient'), - event: () => (this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient), + event: () => { this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient; }, // prettier-ignore icon: 'paint-brush', }); - layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields), icon: 'paint-brush' }); - layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle), icon: 'paint-brush' }); + layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => { this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields; }, icon: 'paint-brush' }); // prettier-ignore + layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => { this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle; }, icon: 'paint-brush' }); // prettier-ignore } ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); if (!Doc.noviceMode) { @@ -240,9 +243,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return ( <EditableView contents={this.dataDoc.title} - display={'block'} + display="block" maxHeight={72} - height={'auto'} + height="auto" GetValue={() => StrCast(this.dataDoc.title)} SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { if (enter && this.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren); @@ -253,22 +256,24 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree ); } - onKey = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { + onKey = (e: React.KeyboardEvent /* , fieldProps: FieldViewProps */) => { if (this.outlineMode && e.key === 'Enter') { e.stopPropagation(); this.makeTextCollection(this.treeChildren); return true; } + return undefined; }; get documentTitle() { return ( <FormattedTextBox + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} fieldKey="text" renderDepth={this._props.renderDepth + 1} isContentActive={this.isContentActive} isDocumentActive={this.isContentActive} - forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not layout_autoHeight + forceAutoHeight // needed to make the title resize even if the rest of the tree view is not layout_autoHeight PanelWidth={this.documentTitleWidth} PanelHeight={this.documentTitleHeight} NativeDimScaling={returnOne} @@ -296,7 +301,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false; - if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20)))); + if (this._renderCount < this.treeChildren.length) + setTimeout( + action(() => { + this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20); + }) + ); return TreeView.GetChildElements( this.treeChildren, this, @@ -326,7 +336,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree this.observeHeight, this.unobserveHeight, this.childContextMenuItems(), - //TODO: [AL] add these this._props.AddToMap, this._props.RemFromMap, this._props.hierarchyIndex, @@ -337,7 +346,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return this.dataDoc === null ? null : ( <div className="collectionTreeView-titleBar" - ref={action((r: any) => (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale))} + ref={action((r: any) => { + (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale); + })} key={this.Document[Id]} style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}> {this.outlineMode ? this.documentTitle : this.editableTitle} @@ -420,7 +431,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}> {!this.buttonMenu && !this.noviceExplainer ? null : ( - <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = DivHeight(r)))}> + <div + className="documentButtonMenu" + ref={action((r: HTMLDivElement | null) => { + r && (this._headerHeight = DivHeight(r)); + })}> {this.buttonMenu} {this.noviceExplainer} </div> @@ -453,7 +468,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree minHeight: '100%', }} onWheel={e => e.stopPropagation()} - onClick={e => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())} + onClick={() => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())} onDrop={this.onTreeDrop}> <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul> </div> @@ -470,13 +485,14 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}> {!(this.Document instanceof Doc) || !this.treeChildren ? null : this.Document.treeView_HasOverlay ? ( <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} pointerEvents={this._props.isContentActive() && SnappingManager.IsDragging ? returnAll : returnNone} - isAnnotationOverlay={true} - isAnnotationOverlayScrollable={true} + isAnnotationOverlay + isAnnotationOverlayScrollable childDocumentsActive={this._props.isContentActive} fieldKey={this._props.fieldKey + '_annotations'} dropAction={dropActionType.move} @@ -500,6 +516,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function addTreeFolder(doc: Doc) { CollectionTreeView.addTreeFolder(doc); }); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 8a2e83ed9..a661cf6a2 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -32,7 +32,7 @@ import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider, StyleProp } from '../StyleProvider'; import { Colors } from '../global/globalEnums'; import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView'; -import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView'; +import { FieldViewProps } from '../nodes/FieldView'; import { KeyValueBox } from '../nodes/KeyValueBox'; import { PresBox, PresMovement } from '../nodes/trails'; import { CollectionDockingView } from './CollectionDockingView'; @@ -42,6 +42,146 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV const _global = (window /* browser */ || global) /* node */ as any; +interface TabMinimapViewProps { + document: Doc; + tabView: () => DocumentView | undefined; + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; + background: () => string; +} +interface TabMiniThumbProps { + miniWidth: () => number; + miniHeight: () => number; + miniTop: () => number; + miniLeft: () => number; +} + +@observer +class TabMiniThumb extends React.Component<TabMiniThumbProps> { + render() { + const { miniWidth, miniHeight, miniLeft, miniTop } = this.props; + return <div className="miniThumb" style={{ width: `${miniWidth()}%`, height: `${miniHeight()}%`, left: `${miniLeft()}%`, top: `${miniTop()}%` }} />; + } +} +@observer +export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps> { + static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { + if (doc) { + switch (property.split(':')[0]) { + case StyleProp.PointerEvents: return 'none'; + case StyleProp.DocContents: { + const background = (() => { + switch (doc.type as DocumentType) { + case DocumentType.PDF: return 'pink'; + case DocumentType.AUDIO: return 'lightgreen'; + case DocumentType.WEB: return 'brown'; + case DocumentType.IMG: return 'blue'; + case DocumentType.MAP: return 'orange'; + case DocumentType.VID: return 'purple'; + case DocumentType.RTF: return 'yellow'; + case DocumentType.COL: return undefined; + default: return 'gray'; + } // prettier-ignore + })(); + return !background ? undefined : <div style={{ width: NumCast(doc._width), height: NumCast(doc._height), position: 'absolute', display: 'block', background }} />; + } + default: return DefaultStyleProvider(doc, props, property); + } // prettier-ignore + } + return undefined; + }; + + @computed get renderBounds() { + const compView = this._props.tabView()?.ComponentView as CollectionFreeFormView; + const bounds = compView?.freeformData?.(true)?.bounds; + if (!bounds) return undefined; + const xbounds = bounds.r - bounds.x; + const ybounds = bounds.b - bounds.y; + const dim = Math.max(xbounds, ybounds); + return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; + } + @computed get xPadding() { + return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l)); + } + @computed get yPadding() { + return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l)); + } + childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + const doc = this._props.document; + const miniSize = this.returnMiniSize(); + doc && + setupMoveUpEvents( + this, + e, + action((moveEv, down: number[], delta: number[]) => { + const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; + doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); + doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); + return false; + }), + emptyFunction, + emptyFunction + ); + }; + popup = () => { + if (!this.renderBounds) return <div />; + const { renderBounds } = this; + const miniWidth = () => (this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100; + const miniHeight = () => (this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100; + const miniLeft = () => 50 + ((NumCast(this._props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2; + const miniTop = () => 50 + ((NumCast(this._props.document._freeform_panY) - renderBounds.cy) / renderBounds.dim) * 100 - miniHeight() / 2; + const miniSize = this.returnMiniSize(); + return ( + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this._props.background() }}> + <CollectionFreeFormView + Document={this._props.document} + docViewPath={returnEmptyDocViewList} + childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. + noOverlay // don't render overlay Docs since they won't scale + isContentActive={emptyFunction} + isAnyChildContentActive={returnFalse} + select={emptyFunction} + isSelected={returnFalse} + dontRegisterView + fieldKey={Doc.LayoutFieldKey(this._props.document)} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + ScreenToLocalTransform={Transform.Identity} + renderDepth={0} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={TabMinimapView.miniStyleProvider} + addDocTab={this._props.addDocTab} + // eslint-disable-next-line no-use-before-define + pinToPres={TabDocView.PinDoc} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} + fitContentsToBox={returnTrue} + xPadding={this.xPadding} + yPadding={this.yPadding} + /> + <div className="miniOverlay" onPointerDown={this.miniDown}> + <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} /> + </div> + </div> + ); + }; + render() { + return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : ( + <div className="miniMap-hidden"> + <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} /> + </div> + ); + } +} + interface TabDocViewProps { documentId: FieldId; keyValue?: boolean; @@ -92,7 +232,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { @action init = (tab: any, doc: Opt<Doc>) => { if (tab.contentItem === tab.header.parent.getActiveContentItem()) this._activated = true; - if (tab.DashDoc !== doc && doc && tab.hasOwnProperty('contentItem') && tab.contentItem.config.type !== 'stack') { + if (tab.DashDoc !== doc && doc && tab.contentItem?.config.type !== 'stack') { tab._disposers = {} as { [name: string]: IReactionDisposer }; tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); tab.DashDoc = doc; @@ -130,17 +270,17 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { setupMoveUpEvents( this, e, - e => - !e.defaultPrevented && - DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY, undefined, () => { + moveEv => + !moveEv.defaultPrevented && + DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), moveEv.clientX, moveEv.clientY, undefined, () => { CollectionDockingView.CloseSplit(doc); }), returnFalse, - action(e => { + action(clickEv => { if (this.view) { SelectionManager.SelectView(this.view, false); const child = getChild(); - simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + simulateMouseClick(child, clickEv.clientX, clickEv.clientY + 30, clickEv.screenX, clickEv.screenY + 30); } else { this._activated = true; setTimeout(() => this.view && SelectionManager.SelectView(this.view, false)); @@ -160,7 +300,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { ({ variant, degree, highlight }) => { const color = highlight?.highlightIndex === Doc.DocBrushStatus.highlighted ? highlight.highlightColor : degree ? ['transparent', variant, variant, 'orange'][degree] : variant; - const textColor = color === variant ? SnappingManager.userColor : lightOrDark(color); + const textColor = color === variant ? SnappingManager.userColor ?? '' : lightOrDark(color); titleEle.style.color = textColor; iconWrap.style.color = textColor; closeWrap.style.color = textColor; @@ -186,19 +326,19 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { ); } // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: MouseEvent) => { + tab.element[0].onmouseenter = () => { if (SnappingManager.IsDragging && tab.contentItem !== tab.header.parent.getActiveContentItem()) { tab.header.parent.setActiveContentItem(tab.contentItem); tab.setActive(true); } this._document && Doc.BrushDoc(this._document); }; - tab.element[0].onmouseleave = (e: MouseEvent) => { + tab.element[0].onmouseleave = () => { this._document && Doc.UnBrushDoc(this._document); }; tab.element[0].oncontextmenu = (e: MouseEvent) => { - let child = getChild(); + const child = getChild(); if (child) { simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); e.stopPropagation(); @@ -220,12 +360,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { () => SelectionManager.IsSelected(this._document), action(selected => { if (selected) this._activated = true; - const toggle = tab.element[0].children[2].children[0] as HTMLInputElement; if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) { undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')(); } - //toggle.style.fontWeight = selected ? 'bold' : ''; - // toggle.style.textTransform = selected ? "uppercase" : ""; }), { fireImmediately: true } ); @@ -233,14 +370,16 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { // highlight the tab when the tab document is brushed in any part of the UI tab._disposers.reactionDisposer = reaction( () => doc?.title, - title => (titleEle.value = title), + title => { + titleEle.value = title; + }, { fireImmediately: true } ); // clean up the tab when it is closed tab.closeElement - .off('click') //unbind the current click handler - .click(function () { + .off('click') // unbind the current click handler + .click(() => { Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); SelectionManager.DeselectAll(); UndoManager.RunInBatch(() => tab.contentItem.remove(), 'delete tab'); @@ -250,7 +389,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { /** * Adds a document to the presentation view - **/ + * */ @action public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; @@ -310,7 +449,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { if (curPres.expandBoolean) pinDoc.presentation_expandInlineButton = true; Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement()); PresBox.Instance?.clearSelectedArray(); - pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array + pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); // Update selected array }); if ( // open the presentation trail if it's not already opened @@ -328,6 +467,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { componentDidMount() { new _global.ResizeObserver( action((entries: any) => { + // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { this._panelWidth = entry.contentRect.width; this._panelHeight = entry.contentRect.height; @@ -374,29 +514,30 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right // lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox - addDocTab = (doc: Doc, location: OpenWhere) => { + addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => { + const docs = docsIn instanceof Doc ? [docsIn] : docsIn; SelectionManager.DeselectAll(); const whereFields = location.split(':'); const keyValue = whereFields.includes(OpenWhereMod.keyvalue); const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; const panelName = whereFields.length > 1 ? whereFields.lastElement() : ''; - if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc); + if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]); // prettier-ignore switch (whereFields[0]) { case undefined: case OpenWhere.lightbox: if (this.layoutDoc?._isLightbox) { - const lightboxView = !doc.annotationOn && DocCast(doc.embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.embedContainer)) : undefined; + const lightboxView = !docs[0].annotationOn && DocCast(docs[0].embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(docs[0].embedContainer)) : undefined; const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.Document)]; if (lightboxView && (!data || data instanceof List)) { - lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.Document)] = new List<Doc>([doc]); + lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.Document)] = new List<Doc>(docs); return true; } } - return LightboxView.Instance.AddDocTab(doc, OpenWhere.lightbox); - case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); - case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, panelName, undefined, keyValue); - case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue); - case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue); + return LightboxView.Instance.AddDocTab(docs[0], OpenWhere.lightbox); + case OpenWhere.close: return CollectionDockingView.CloseSplit(docs[0], whereMods); + case OpenWhere.replace: return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue); + case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue); } }; remDocTab = (doc: Doc | Doc[]) => { @@ -410,12 +551,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame); static Activate = (tabDoc: Doc) => { - const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue); + const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue); tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) return tab !== undefined; }; @action - focusFunc = (doc: Doc, options: FocusViewOptions) => { + focusFunc = () => { if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) } @@ -498,8 +639,12 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { } this._lastTab = this.tab; (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); - DocServer.GetRefField(this._props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document))); - new _global.ResizeObserver(action((entries: any) => this._forceInvalidateScreenToLocal++)).observe(ref); + DocServer.GetRefField(this._props.documentId).then( + action(doc => { + doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document); + }) + ); + new _global.ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref); } }}> {this.docView} @@ -507,141 +652,3 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { ); } } - -interface TabMinimapViewProps { - document: Doc; - tabView: () => DocumentView | undefined; - addDocTab: (doc: Doc, where: OpenWhere) => boolean; - PanelWidth: () => number; - PanelHeight: () => number; - background: () => string; -} -interface TabMiniThumbProps { - miniWidth: () => number; - miniHeight: () => number; - miniTop: () => number; - miniLeft: () => number; -} - -@observer -class TabMiniThumb extends React.Component<TabMiniThumbProps> { - render() { - const { miniWidth, miniHeight, miniLeft, miniTop } = this.props; - return <div className="miniThumb" style={{ width: `${miniWidth()}%`, height: `${miniHeight()}%`, left: `${miniLeft()}%`, top: `${miniTop()}%` }} />; - } -} -@observer -export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps> { - static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { - if (doc) { - switch (property.split(':')[0]) { - case StyleProp.PointerEvents: return 'none'; - case StyleProp.DocContents: { - const background = (() => { - switch (doc.type as DocumentType) { - case DocumentType.PDF: return 'pink'; - case DocumentType.AUDIO: return 'lightgreen'; - case DocumentType.WEB: return 'brown'; - case DocumentType.IMG: return 'blue'; - case DocumentType.MAP: return 'orange'; - case DocumentType.VID: return 'purple'; - case DocumentType.RTF: return 'yellow'; - case DocumentType.COL: return undefined; - default: return 'gray'; - } // prettier-ignore - })(); - return !background ? undefined : <div style={{ width: NumCast(doc._width), height: NumCast(doc._height), position: 'absolute', display: 'block', background }} />; - } - default: return DefaultStyleProvider(doc, props, property); - } // prettier-ignore - } - }; - - @computed get renderBounds() { - const compView = this._props.tabView()?.ComponentView as CollectionFreeFormView; - const bounds = compView?.freeformData?.(true)?.bounds; - if (!bounds) return undefined; - const xbounds = bounds.r - bounds.x; - const ybounds = bounds.b - bounds.y; - const dim = Math.max(xbounds, ybounds); - return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; - } - @computed get xPadding() { - return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l)); - } - @computed get yPadding() { - return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l)); - } - childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - const doc = this._props.document; - const miniSize = this.returnMiniSize(); - doc && - setupMoveUpEvents( - this, - e, - action((e: PointerEvent, down: number[], delta: number[]) => { - const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; - doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); - doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); - return false; - }), - emptyFunction, - emptyFunction - ); - }; - popup = () => { - if (!this.renderBounds) return <></>; - const renderBounds = this.renderBounds; - const miniWidth = () => (this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100; - const miniHeight = () => (this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) / renderBounds.dim) * 100; - const miniLeft = () => 50 + ((NumCast(this._props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2; - const miniTop = () => 50 + ((NumCast(this._props.document._freeform_panY) - renderBounds.cy) / renderBounds.dim) * 100 - miniHeight() / 2; - const miniSize = this.returnMiniSize(); - return ( - <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this._props.background() }}> - <CollectionFreeFormView - Document={this._props.document} - docViewPath={returnEmptyDocViewList} - childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. - noOverlay={true} // don't render overlay Docs since they won't scale - isContentActive={emptyFunction} - isAnyChildContentActive={returnFalse} - select={emptyFunction} - isSelected={returnFalse} - dontRegisterView={true} - fieldKey={Doc.LayoutFieldKey(this._props.document)} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - ScreenToLocalTransform={Transform.Identity} - renderDepth={0} - whenChildContentsActiveChanged={emptyFunction} - focus={emptyFunction} - styleProvider={TabMinimapView.miniStyleProvider} - addDocTab={this._props.addDocTab} - pinToPres={TabDocView.PinDoc} - childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} - searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} - fitContentsToBox={returnTrue} - xPadding={this.xPadding} - yPadding={this.yPadding} - /> - <div className="miniOverlay" onPointerDown={this.miniDown}> - <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} /> - </div> - </div> - ); - }; - render() { - return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : ( - <div className="miniMap-hidden"> - <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SnappingManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} /> - </div> - ); - } -} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 20323a521..fab8e3892 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -55,7 +55,7 @@ export interface TreeViewProps { treeViewParent: Doc; renderDepth: number; dragAction: dropActionType; - addDocTab: (doc: Doc, where: OpenWhere) => boolean; + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => boolean; @@ -200,11 +200,11 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { } return false; }; - @undoBatch remove = (doc: Doc | Doc[], key: string) => { + @undoBatch remove = (docs: Doc | Doc[], key: string) => { this.treeView._props.select(false); - const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement()); + const ind = DocListCast(this.dataDoc[key]).indexOf(docs instanceof Doc ? docs : docs.lastElement()); - const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); + const res = (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.treeView.DocumentView?.())?.select(false); return res; }; @@ -318,7 +318,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { this._props.hierarchyIndex !== undefined && this._props.AddToMap?.(this.Document, this._props.hierarchyIndex); } - onDragUp = (e: PointerEvent) => { + onDragUp = () => { document.removeEventListener('pointerup', this.onDragUp, true); document.removeEventListener('pointermove', this.onDragMove, true); }; @@ -403,7 +403,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { if (!this._header.current) return false; const rect = this._header.current.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); + const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || !!(!before && this.treeViewOpen && this.childDocs?.length); if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor(); const destDoc = this.Document; @@ -431,14 +431,14 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return false; }; - localAdd = (doc: Doc | Doc[]) => { - const innerAdd = (doc: Doc) => { + localAdd = (docs: Doc | Doc[]): boolean => { + const innerAdd = (doc: Doc): boolean => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)); return added; }; - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); + return (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); }; dropping: boolean = false; @@ -513,16 +513,16 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const leftOffset = observable({ width: 0 }); const expandedWidth = () => this._props.panelWidth() - leftOffset.width; if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); - const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc); - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => { - const innerAdd = (doc: Doc) => { + const remDoc = (docs: Doc | Doc[]) => this.remove(docs, key); + const moveDoc = (docs: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(docs, target, addDoc); + const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => { + const innerAdd = (iDoc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; - const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)); + const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, iDoc, addBefore, before, false, true); + dataIsComputed && Doc.SetContainer(iDoc, DocCast(this.Document.embedContainer)); return added; }; - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); + return (docs instanceof Doc ? [docs] : docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean); }; contentElement = TreeView.GetChildElements( contents instanceof Doc ? [contents] : DocListCast(contents), @@ -643,7 +643,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return added; }; - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); + const addDoc = (docs: Doc | Doc[], addBefore?: Doc, before?: boolean) => (docs instanceof Doc ? [docs] : docs).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs; let downX = 0; let downY = 0; @@ -911,7 +911,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { // prettier-ignore switch (property.split(':')[0]) { case StyleProp.Opacity: return this.treeView.outlineMode ? undefined : 1; - case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined;//StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); + case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined; // StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); case StyleProp.Highlighting: if (this.treeView.outlineMode) return undefined; break; case StyleProp.BoxShadow: return undefined; @@ -1192,7 +1192,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const pt = [de.clientX, de.clientY]; const rect = this._header.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); + const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || !!(!before && this.treeViewOpen && this.childDocs?.length); this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, dropActionType.copy, undefined, undefined, false, false)); }; @@ -1259,7 +1259,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, dragAction: dropActionType, - addDocTab: (doc: Doc, where: OpenWhere) => boolean, + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean, styleProvider: undefined | StyleProviderFuncType, screenToLocalXf: () => Transform, isContentActive: (outsideReaction?: boolean) => boolean, @@ -1282,11 +1282,6 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { hierarchyIndex?: number[], renderCount?: number ) { - const viewSpecScript = Cast(treeViewParent.viewSpecScript, ScriptField); - if (viewSpecScript) { - childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); - } - const docs = TreeView.sortDocs(childDocs, StrCast(treeViewParent.treeView_SortCriterion, TreeSort.WhenAdded)); const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView._props.NativeDimScaling?.() || 1); const treeViewRefs = new Map<Doc, TreeView | undefined>(); @@ -1327,7 +1322,6 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { dataDoc={pair.data} treeViewParent={treeViewParent} prevSibling={docs[i]} - // TODO: [AL] add these hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined} AddToMap={AddToMap} RemFromMap={RemFromMap} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 29d835b28..65a529d62 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { Doc, DocListCast, FieldType, FieldResult } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { StrCast } from '../../../../fields/Types'; -import { DocumentManager } from '../../../util/DocumentManager'; import { LinkManager } from '../../../util/LinkManager'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton'; @@ -49,8 +48,8 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio setupStates = () => { this._originalbackground = StrCast(this._props.Document[DocData].backgroundColor); // state entry functions - const setBackground = (colour: string) => () => {this._props.Document[DocData].backgroundColor = colour;} // prettier-ignore - const setOpacity = (opacity: number) => () => {this._props.LayoutDoc.opacity = opacity;} // prettier-ignore + // const setBackground = (colour: string) => () => {this._props.Document[DocData].backgroundColor = colour;} // prettier-ignore + // const setOpacity = (opacity: number) => () => {this._props.LayoutDoc.opacity = opacity;} // prettier-ignore // arc transition trigger conditions const firstDoc = () => (this._props.childDocs().length ? this._props.childDocs()[0] : undefined); const numDocs = () => this._props.childDocs().length; @@ -73,7 +72,6 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio let trail: number; - const trailView = () => DocumentManager.Instance.DocumentViews.find(view => view.Document === Doc.MyTrails); const presentationMode = () => Doc.ActivePresentation?.presentation_status; // set of states @@ -83,6 +81,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio docCreated: [() => numDocs(), () => { docX = firstDoc()?.x; docY = firstDoc()?.y; + // eslint-disable-next-line no-use-before-define return oneDoc; }], } @@ -93,18 +92,20 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio { // docCreated: [() => numDocs() > 1, () => multipleDocs], docDeleted: [() => numDocs() < 1, () => start], - docMoved: [() => (docX && docX != docNewX()) || (docY && docY != docNewY()), () => { + docMoved: [() => (docX && docX !== docNewX()) || (docY && docY !== docNewY()), () => { docX = firstDoc()?.x; docY = firstDoc()?.y; + // eslint-disable-next-line no-use-before-define return movedDoc; }], } ); // prettier-ignore const movedDoc = InfoState( - 'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (\":\")', + 'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (":")', { - docCreated: [() => numDocs() == 2, () => multipleDocs], + // eslint-disable-next-line no-use-before-define + docCreated: [() => numDocs() === 2, () => multipleDocs], docDeleted: [() => numDocs() < 1, () => start], }, 'dash-colon-menu.gif', @@ -114,6 +115,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio const multipleDocs = InfoState( 'Let\'s create a new link. Click the link icon on one of your documents.', { + // eslint-disable-next-line no-use-before-define linkStarted: [() => linkStart(), () => startedLink], docRemoved: [() => numDocs() < 2, () => oneDoc], }, @@ -124,6 +126,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio 'Now click the highlighted link icon on your other document.', { linkUnstart: [() => linkUnstart(), () => multipleDocs], + // eslint-disable-next-line no-use-before-define linkCreated: [() => numDocLinks(), () => madeLink], docRemoved: [() => numDocs() < 2, () => oneDoc], }, @@ -136,6 +139,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio linkCreated: [() => !numDocLinks(), () => multipleDocs], linkViewed: [() => linkMenuOpen(), () => { alert(numDocLinks() + " cheer for " + numDocLinks() + " link!"); + // eslint-disable-next-line no-use-before-define return viewedLink; }], }, @@ -147,10 +151,12 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio { linkDeleted: [() => !numDocLinks(), () => multipleDocs], docRemoved: [() => numDocs() < 2, () => oneDoc], - docCreated: [() => numDocs() == 3, () => { + docCreated: [() => numDocs() === 3, () => { trail = pin().length; + // eslint-disable-next-line no-use-before-define return presentDocs; }], + // eslint-disable-next-line no-use-before-define activePen: [() => activeTool() === InkTool.Pen, () => penMode], }, 'documentation.png', @@ -164,6 +170,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio () => pin().length > trail, () => { trail = pin().length; + // eslint-disable-next-line no-use-before-define return pinnedDoc1; }, ], @@ -186,11 +193,13 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio () => pin().length > trail, () => { trail = pin().length; + // eslint-disable-next-line no-use-before-define return pinnedDoc2; }, ], // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], + // eslint-disable-next-line no-use-before-define autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], docRemoved: [() => numDocs() < 3, () => viewedLink], }); @@ -200,11 +209,13 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio () => pin().length > trail, () => { trail = pin().length; + // eslint-disable-next-line no-use-before-define return pinnedDoc3; }, ], // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], + // eslint-disable-next-line no-use-before-define autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], docRemoved: [() => numDocs() < 3, () => viewedLink], }); @@ -219,6 +230,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio ], // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], + // eslint-disable-next-line no-use-before-define autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], docRemoved: [() => numDocs() < 3, () => viewedLink], }); @@ -236,15 +248,18 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio const manualPresentationMode = InfoState("You're in manual presentation mode.", { // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], + // eslint-disable-next-line no-use-before-define autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], docRemoved: [() => numDocs() < 3, () => viewedLink], - docCreated: [() => numDocs() == 4, () => completed], + // eslint-disable-next-line no-use-before-define + docCreated: [() => numDocs() === 4, () => completed], }); const autoPresentationMode = InfoState("You're in auto presentation mode.", { // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], docRemoved: [() => numDocs() < 3, () => viewedLink], + // eslint-disable-next-line no-use-before-define docCreated: [() => numDocs() === 4, () => completed], }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index a0b96c75a..3970c6807 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-use-before-define */ import { Doc, Field, FieldType, FieldResult } from '../../../../fields/Doc'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; @@ -48,7 +49,7 @@ export interface PoolData { export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; - inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden + inkMask?: number; // sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden } function toLabel(target: FieldResult<FieldType>) { if (typeof target === 'number' || Number(target)) { @@ -84,9 +85,9 @@ interface PivotColumn { filters: string[]; } -export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) { const docMap = new Map<string, PoolData>(); - childPairs.forEach(({ layout, data }, i) => { + childPairs.forEach(({ layout, data }) => { docMap.set(layout[Id], { x: NumCast(layout.x), y: NumCast(layout.y), @@ -97,10 +98,15 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc replica: '', }); }); + // eslint-disable-next-line no-use-before-define return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); } -export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +function toNumber(val: FieldResult<FieldType>) { + return val === undefined ? undefined : NumCast(val, Number(StrCast(val))); +} + +export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) { const docMap = new Map<string, PoolData>(); const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)]; const burstScale = NumCast(pivotDoc._starburstDocScale, 1); @@ -132,7 +138,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do let nonNumbers = 0; const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author'; - childPairs.map(pair => { + childPairs.forEach(pair => { const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null); const num = toNumber(pair.layout[pivotFieldKey]); @@ -184,11 +190,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do const textlen = Array.from(pivotColumnGroups.keys()) .map(c => getTextWidth(toLabel(c), desc)) .reduce((p, c) => Math.max(p, c), 0 as number); - const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2); + const maxText = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2); const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1); const colWidth = panelDim[0] / pivotColumnGroups.size; - const colHeight = panelDim[1] - max_text; + const colHeight = panelDim[1] - maxText; let numCols = 0; let bestArea = 0; let pivotAxisWidth = 0; @@ -212,7 +218,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do let x = 0; const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort(); sortedPivotKeys.forEach(key => { - const val = pivotColumnGroups.get(key)!; + const val = pivotColumnGroups.get(key); let y = 0; let xCount = 0; const text = toLabel(key); @@ -222,11 +228,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do x, y: pivotAxisWidth, width: pivotAxisWidth * expander * numCols, - height: max_text, + height: maxText, fontSize, payload: val, }); - val.docs.forEach((doc, i) => { + val?.docs.forEach((doc, i) => { const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); @@ -262,14 +268,11 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do payload: pivotColumnGroups.get(key)!.filters, })); groupNames.push(...dividers); - return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []); -} - -function toNumber(val: FieldResult<FieldType>) { - return val === undefined ? undefined : NumCast(val, Number(StrCast(val))); + // eslint-disable-next-line no-use-before-define + return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []); } -export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps?: any) { +export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps?: any */) { const fieldKey = 'data'; const pivotDateGroups = new Map<number, Doc[]>(); const docMap = new Map<string, PoolData>(); @@ -340,6 +343,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) { groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); } + // eslint-disable-next-line no-use-before-define layoutDocsAtTime(keyDocs, key); }); if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) { @@ -404,7 +408,7 @@ function normalizeResults( Array.from(docMap.entries()) .filter(ele => ele[1].pair) - .map(ele => { + .forEach(ele => { const newPosRaw = ele[1]; if (newPosRaw) { const newPos: PoolData = { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a70713429..d9c988d54 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import { Bezier } from 'bezier-js'; @@ -22,7 +23,6 @@ import { TraceMobx } from '../../../../fields/util'; import { Gestures, PointData } from '../../../../pen-gestures/GestureTypes'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; import { aggregateBounds, emptyFunction, intersectRect, Utils } from '../../../../Utils'; -import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -48,6 +48,7 @@ import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PresBox } from '../../nodes/trails/PresBox'; +// eslint-disable-next-line import/extensions import { CreateImage } from '../../nodes/WebBoxRenderer'; import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; @@ -76,7 +77,7 @@ export interface collectionFreeformViewProps { @observer export class CollectionFreeFormView extends CollectionSubView<Partial<collectionFreeformViewProps>>() { public get displayName() { - return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')'; + return 'CollectionFreeFormView(' + (this.Document.title?.toString() ?? '') + ')'; } // this makes mobx trace() statements more descriptive @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); @@ -94,8 +95,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection super(props); makeObservable(this); } - @observable - public static ShowPresPaths = false; private _panZoomTransitionTimer: any; private _lastX: number = 0; @@ -238,7 +237,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; @observable _keyframeEditing = false; - @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); + @action setKeyFrameEditing = (set: boolean) => { + this._keyframeEditing = set; + }; getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this._props.onBrowseClickScript?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); @@ -256,7 +257,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); - zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); + zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`); ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -272,7 +273,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - if ((retVal = this._props.addDocument?.(newBox) || false)) { + retVal = this._props.addDocument?.(newBox) || false; + if (retVal) { this.bringToFront(newBox); this.updateCluster(newBox); } @@ -282,15 +284,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } if (retVal) { const newBoxes = newBox instanceof Doc ? [newBox] : newBox; - for (const newBox of newBoxes) { - if (newBox.activeFrame !== undefined) { - const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}_indexed`]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]); - delete newBox.activeFrame; - CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i])); + newBoxes.forEach(box => { + if (box.activeFrame !== undefined) { + const vals = CollectionFreeFormDocumentView.animFields.map(field => box[field.key]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete box[`${field.key}_indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete box[field.key]); + delete box.activeFrame; + CollectionFreeFormDocumentView.animFields.forEach((field, i) => { + field.key !== 'opacity' && (box[field.key] = vals[i]); + }); } - } + }); if (this.Document._currentFrame !== undefined && !this._props.isAnnotationOverlay) { CollectionFreeFormDocumentView.setupKeyframes(newBoxes, NumCast(this.Document._currentFrame), true); } @@ -313,14 +317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; focus = (anchor: Doc, options: FocusViewOptions) => { - if (this._lightboxDoc) return; + if (this._lightboxDoc) return undefined; if (anchor === this.Document) { // if (options.willZoomCentered && options.zoomScale) { // this.fitContentOnce(); // options.didMove = true; // } } - if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) return; + // prettier-ignore + if (anchor.type !== DocumentType.CONFIG && + !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor) && // + !this.childLayoutPairs.map(pair => pair.layout).includes(anchor)) { + return undefined; + } const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); @@ -336,12 +345,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.setPan(panX, panY, focusTime, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow return focusTime; } + return undefined; }; getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> => new Promise<Opt<DocumentView>>(res => { if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false); - if (doc === this.Document) return res(this.DocumentView?.()); + if (doc === this.Document) { + res(this.DocumentView?.()); + return; + } const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv)); findDoc(dv => res(dv)); }); @@ -357,7 +370,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .map(pair => pair.layout) .slice() .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - zsorted.forEach((doc, index) => (doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1)); + zsorted.forEach((doc, index) => { + doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1; + }); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; @@ -410,7 +425,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection let added = false; // do nothing if link is dropped into any freeform view parent of dragged document const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }); - added = this._props.addDocument?.(source) ? true : false; + added = !!this._props.addDocument?.(source); de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed if (de.complete.linkDocument) { de.complete.linkDocument.layout_isSvg = true; @@ -425,8 +440,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData); - else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); - else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); + if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; @@ -487,8 +502,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @action - updateClusters(_freeform_useClusters: boolean) { - this.Document._freeform_useClusters = _freeform_useClusters; + updateClusters(useClusters: boolean) { + this.Document._freeform_useClusters = useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -498,12 +513,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.Document._freeform_useClusters) { const docFirst = docs[0]; - docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); + docs.forEach(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); const preferredInd = NumCast(docFirst.layout_cluster); - docs.map(doc => (doc.layout_cluster = -1)); + docs.forEach(doc => { + doc.layout_cluster = -1; + }); docs.map(doc => this._clusterSets.map((set, i) => - set.map(member => { + set.forEach(member => { if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { docFirst.layout_cluster = i; } @@ -518,19 +535,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ) { docFirst.layout_cluster = preferredInd; } - this._clusterSets.map((set, i) => { + this._clusterSets.forEach((set, i) => { if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { docFirst.layout_cluster = i; } }); if (docFirst.layout_cluster === -1) { - docs.map(doc => { + docs.forEach(doc => { doc.layout_cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); - docs.map(doc => this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc)); + docs.forEach(doc => { + this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc); + }); } childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child)); } @@ -573,17 +592,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (doc && this.childDocList?.includes(doc)) switch (property.split(':')[0]) { case StyleProp.BackgroundColor: - const cluster = NumCast(doc?.layout_cluster); - if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) { - if (this._clusterSets.length <= cluster) { - setTimeout(() => doc && this.updateCluster(doc)); - } else { - // choose a cluster color from a palette - const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; - styleProp = colors[cluster % colors.length]; - const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); - // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set?.map(s => (styleProp = StrCast(s.backgroundColor))); + { + const cluster = NumCast(doc?.layout_cluster); + if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) { + if (this._clusterSets.length <= cluster) { + setTimeout(() => doc && this.updateCluster(doc)); + } else { + // choose a cluster color from a palette + const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; + styleProp = colors[cluster % colors.length]; + const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); + // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document + set?.forEach(s => { + styleProp = StrCast(s.backgroundColor); + }); + } } } break; @@ -591,6 +614,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (doc && this.Document._currentFrame !== undefined) { return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor; } + break; + default: } return styleProp; }; @@ -626,9 +651,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case InkTool.None: if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1, false); } break; + default: } } } @@ -639,13 +665,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @undoBatch onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { switch (ge.gesture) { - default: + // case Gestures.Rectangle: + // { + // const strokes = this.getActiveDocuments() + // .filter(doc => doc.type === DocumentType.INK) + // .map(i => { + // const d = Cast(i.stroke, InkField); + // const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); + // const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); + // return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y })); + // }); + + // CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {}); + // } + // break; + case Gestures.Text: + if (ge.text) { + const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y); + this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); + e.stopPropagation(); + } + break; case Gestures.Line: case Gestures.Circle: case Gestures.Rectangle: case Gestures.Triangle: case Gestures.Stroke: - const points = ge.points; + default: { + const { points } = ge; const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; const inkDoc = Docs.Create.InkDocument( @@ -664,29 +711,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } this.addDocument(inkDoc); e.stopPropagation(); - break; - case Gestures.Rectangle: - const strokes = this.getActiveDocuments() - .filter(doc => doc.type === DocumentType.INK) - .map(i => { - const d = Cast(i.stroke, InkField); - const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); - const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); - return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y })); - }); - - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {}); - break; - case Gestures.Text: - if (ge.text) { - const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y); - this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); - e.stopPropagation(); - } + } } }; @action - onEraserUp = (e: PointerEvent): void => { + onEraserUp = (): void => { this._deleteList.forEach(ink => ink._props.removeDocument?.(ink.Document)); this._deleteList = []; this._batch?.end(); @@ -792,7 +821,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.childDocs .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) - .map(inkView => ({ inkViewBounds: inkView!.getBounds, inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) + .map(inkView => inkView!) + .map(inkView => ({ inkViewBounds: inkView.getBounds, inkStroke: inkView.ComponentView as InkingStroke, inkView })) .filter( ({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap @@ -807,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // Convert from screen space to ink space for the intersection. const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); const currPointInkSpace = inkStroke.ptFromScreen(currPoint); - for (var i = 0; i < inkData.length - 3; i += 4) { + for (let i = 0; i < inkData.length - 3; i += 4) { const rawIntersects = InkField.Segment(inkData, i).intersects({ // compute all unique intersections p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, @@ -833,16 +863,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => { const segments: Segment[] = []; - var segment: Segment = []; - var startSegmentT = 0; + let segment: Segment = []; + let startSegmentT = 0; const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData(); // This iterates through all segments of the curve and splits them where they intersect another curve. // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted) - for (var i = 0; i < inkData.length - 3; i += 4) { + for (let i = 0; i < inkData.length - 3; i += 4) { const inkSegment = InkField.Segment(inkData, i); // Getting all t-value intersections of the current curve with all other curves. const tVals = this.getInkIntersections(i, ink, inkSegment).sort(); if (tVals.length) { + // eslint-disable-next-line no-loop-func tVals.forEach((t, index) => { const docCurveTVal = t + Math.floor(i / 4); if (excludeT < startSegmentT || excludeT > docCurveTVal) { @@ -895,9 +926,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point)); const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt)); - for (var j = 0; j < otherCtrlPts.length - 3; j += 4) { + for (let j = 0; j < otherCtrlPts.length - 3; j += 4) { const neighboringSegment = i === j || i === j - 4 || i === j + 4; // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. + // eslint-disable-next-line no-continue if (ink?.Document === otherInk.Document && neighboringSegment) continue; const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y }))); @@ -908,7 +940,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) { tVals.push(apt.t); } - this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => { + this.bintersects(curve, otherCurve).forEach((val: string | number /* , i: number */) => { // Converting the Bezier.js Split type to a t-value number. const t = +val.toString().split('/')[0]; if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical). @@ -971,8 +1003,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.scrollPan({ deltaX: -deltaX * this.screenToFreeformContentsXf.Scale, deltaY: e.shiftKey ? 0 : -deltaY * this.screenToFreeformContentsXf.Scale }); break; } - default: + // eslint-disable-next-line no-fallthrough case freeformScrollMode.Zoom: + default: if ((e.ctrlKey || !scrollable) && this._props.isContentActive()) { this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this._props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? // e.preventDefault(); @@ -982,7 +1015,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { + setPan(panXIn: number, panYIn: number, panTime: number = 0, clamp: boolean = false) { + let panX = panXIn; + let panY = panYIn; // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code. if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction(); @@ -1034,11 +1069,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection scale * NumCast(this.dataDoc._panY_max, nativeHeight) + (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); - if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) { - } else if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) { - const maxPanY = minPanY + fitYscroll; - const relTop = (panY - minPanY) / (maxPanY - minPanY); - setTimeout(() => (this.layoutDoc.layout_scrollTop = relTop * maxScrollTop), 10); + if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) { + const maxPanScrollY = minPanY + fitYscroll; + const relTop = (panY - minPanY) / (maxPanScrollY - minPanY); + setTimeout(() => { + this.layoutDoc.layout_scrollTop = relTop * maxScrollTop; + }, 10); newPanY = minPanY; } !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); @@ -1090,7 +1126,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._panZoomTransition = transitionTime; this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer); this._panZoomTransitionTimer = setTimeout( - action(() => (this._panZoomTransition = 0)), + action(() => { + this._panZoomTransition = 0; + }), transitionTime ); }; @@ -1173,6 +1211,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation?.(); return this.createTextDocCopy(fieldProps, !e.altKey && e.key !== 'Tab'); } + return undefined; }; @computed get childPointerEvents() { const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine); @@ -1194,6 +1233,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const childData = entry.pair.data; return ( <CollectionFreeFormDocumentView + // eslint-disable-next-line react/jsx-props-no-spreading {...OmitKeys(entry, ['replica', 'pair']).omit} key={childLayout[Id] + (entry.replica || '')} Document={childLayout} @@ -1205,7 +1245,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isGroupActive={this._props.isGroupActive} renderDepth={this._props.renderDepth + 1} hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)} - suppressSetHeight={this.layoutEngine ? true : false} + suppressSetHeight={!!this.layoutEngine} RenderCutoffProvider={this.renderCutoffProvider} CollectionFreeFormView={this} LayoutTemplate={childLayout.z ? undefined : this._props.childLayoutTemplate} @@ -1239,37 +1279,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /> ); } - addDocTab = action((doc: Doc, where: OpenWhere) => { - if (this._props.isAnnotationOverlay) return this._props.addDocTab(doc, where); + addDocTab = action((docsIn: Doc | Doc[], where: OpenWhere) => { + const docs = docsIn instanceof Doc ? [docsIn] : docsIn; + if (this._props.isAnnotationOverlay) return this._props.addDocTab(docs, where); switch (where) { case OpenWhere.inParent: - return this._props.addDocument?.(doc) || false; - case OpenWhere.inParentFromScreen: - const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer); + return this._props.addDocument?.(docs) || false; + case OpenWhere.inParentFromScreen: { + const docContext = DocCast(docs[0]?.embedContainer); return ( (this.addDocument?.( - (doc instanceof Doc ? [doc] : doc).map(doc => { - const pt = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - doc.x = pt[0]; - doc.y = pt[1]; + (docs instanceof Doc ? [docs] : docs).map(doc => { + [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); return doc; }) ) && (!docContext || this._props.removeDocument?.(docContext))) || false ); + } case undefined: case OpenWhere.lightbox: - if (this.layoutDoc._isLightbox) { - this._lightboxDoc = doc; - return true; - } - if (doc === this.Document || this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc)) { - if (doc.hidden) doc.hidden = false; - return true; + { + const firstDoc = docs[0]; + if (this.layoutDoc._isLightbox) { + this._lightboxDoc = firstDoc; + return true; + } + if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) { + if (firstDoc.hidden) firstDoc.hidden = false; + return true; + } } + break; + default: } - return this._props.addDocTab(doc, where); + return this._props.addDocTab(docs, where); }); @observable _lightboxDoc: Opt<Doc> = undefined; @@ -1314,9 +1359,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); }; - viewDefsToJSX = (views: ViewDefBounds[]) => { - return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); - }; + viewDefsToJSX = (views: ViewDefBounds[]) => (!Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!)); viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { const { x, y, z } = viewDef; @@ -1337,7 +1380,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ), bounds: viewDef, }; - } else if (viewDef.type === 'div') { + } + if (viewDef.type === 'div') { return [x, y].some(val => val === undefined) ? undefined : { @@ -1353,9 +1397,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection bounds: viewDef, }; } + return undefined; } renderCutoffProvider = computedFn( + // eslint-disable-next-line prefer-arrow-callback function renderCutoffProvider(this: any, doc: Doc) { return this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + ''); }.bind(this) @@ -1369,7 +1415,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } doFreeformLayout(poolData: Map<string, PoolData>) { - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); return [] as ViewDefResult[]; } @@ -1385,6 +1431,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) }; + default: } return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } @@ -1469,7 +1516,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.pointerevents = reaction( () => this.childPointerEvents, - pointerevents => (this._childPointerEvents = pointerevents as any), + pointerevents => { + this._childPointerEvents = pointerevents as any; + }, { fireImmediately: true } ); @@ -1643,7 +1692,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - onContextMenu = (e: React.MouseEvent) => { + onContextMenu = () => { if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription('Appearance...'); @@ -1654,7 +1703,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); return; } - !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); + !Doc.noviceMode && + Doc.UserDoc().defaultTextLayout && + appearanceItems.push({ + description: 'Reset default note style', + event: () => { + Doc.UserDoc().defaultTextLayout = undefined; + }, + icon: 'eye', + }); appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); @@ -1669,8 +1726,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const optionItems = options && 'subitems' in options ? options.subitems : []; !this._props.isAnnotationOverlay && !Doc.noviceMode && - optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' }); - this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor)), icon: 'palette' }); + optionItems.push({ + description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', + event: action(() => { + this._showAnimTimeline = !this._showAnimTimeline; + }), + icon: 'eye', + }); + this._props.renderDepth && + optionItems.push({ + description: 'Use Background Color as Default', + event: () => { + Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor); + }, + icon: 'palette', + }); this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' }); @@ -1750,7 +1820,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } - showPresPaths = () => CollectionFreeFormView.ShowPresPaths; + showPresPaths = () => SnappingManager.ShowPresPaths; brushedView = () => this._brushedView; gridColor = () => DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor))) @@ -1908,7 +1978,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ) : ( <> {this._firstRender ? this.placeholder : this.marqueeView} - {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} + { + // eslint-disable-next-line no-use-before-define + this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} /> + } {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />} </> )} @@ -1920,7 +1993,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observer class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { render() { - // eslint-disable-next-line react/destructuring-assignment return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore } } @@ -1959,6 +2031,7 @@ ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) { const selView = SelectionManager.Views; if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent'; runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.())); + return undefined; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function pinWithView(pinContent: boolean) { @@ -1989,7 +2062,7 @@ ScriptingGlobals.add(function datavizFromSchema() { if (!view.layoutDoc.schema_columnKeys) { view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']); } - const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); + const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key !== 'text'); if (!keys) return; const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b03e435ce..2f9cc49e0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -196,7 +196,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps const eachCell = ns.join('\t').split('\t'); let eachRow = []; for (let i = 1; i < eachCell.length; i++) { - eachRow.push(eachCell[i].replace(/\,/g, '')); + eachRow.push(eachCell[i].replace(/,/g, '')); if (i % headers.length === 0) { csvRows.push(eachRow); eachRow = []; @@ -377,7 +377,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }); @undoBatch - pileup = action((e: KeyboardEvent | React.PointerEvent | undefined) => { + pileup = action(() => { const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); selected.forEach(d => this._props.removeDocument?.(d)); @@ -485,7 +485,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }); @undoBatch - summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => { + summary = action(() => { const selected = this.marqueeSelect(false).map(d => { this._props.removeDocument?.(d); d.x = NumCast(d.x) - this.Bounds.left; @@ -530,8 +530,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps (e as any).propagationIsStopped = true; if (e.key === 'g') this.collection(e, true); if (e.key === 'c' || e.key === 't') this.collection(e); - if (e.key === 's' || e.key === 'S') this.summary(e); - if (e.key === 'p') this.pileup(e); + if (e.key === 's' || e.key === 'S') this.summary(); + if (e.key === 'p') this.pileup(); this.cleanupInteractions(false); } if (e.key === 'r' || e.key === ' ') { diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 08eb35586..ee30006ae 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable react/jsx-props-no-spreading */ +/* eslint-disable no-use-before-define */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Popup, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -26,7 +29,7 @@ import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; -import { OpenWhere, returnEmptyDocViewList } from '../../nodes/DocumentView'; +import { returnEmptyDocViewList } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -64,8 +67,8 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro makeObservable(this); } - static addFieldDoc = (doc: Doc, where: OpenWhere) => { - DocFocusOrOpen(doc); + static addFieldDoc = (doc: Doc | Doc[] /* , where: OpenWhere */) => { + DocFocusOrOpen(doc instanceof Doc ? doc : doc[0]); return true; }; public static renderProps(props: SchemaTableCellProps) { @@ -186,7 +189,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro return ( <div className="schema-table-cell" - onPointerDown={action(e => !this.selected && this._props.selectCell(this._props.Document, this._props.col))} + onPointerDown={action(() => !this.selected && this._props.selectCell(this._props.Document, this._props.col))} style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> {this.content} </div> @@ -207,7 +210,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro choosePath(url: URL) { if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return ClientUtils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; // Why is this here — good question const ext = extname(url.href); return url.href.replace(ext, '_s' + ext); @@ -246,7 +249,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro }; @action - removeHoverPreview = (e: React.PointerEvent) => { + removeHoverPreview = () => { if (!this._previewRef) return; document.body.removeChild(this._previewRef); }; @@ -258,7 +261,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro const height = this._props.rowHeight() ? this._props.rowHeight() - (this._props.padding || 6) * 2 : undefined; const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality - return <img src={this.url} width={width ? width : undefined} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />; + return <img src={this.url} width={width || undefined} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />; } } @@ -282,15 +285,15 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to date && (this._props.Document[this._props.fieldKey] = new DateField(date)); - //} + // } }, 'date change'); render() { - const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); + const { pointerEvents } = SchemaTableCell.renderProps(this._props); return ( <> <div style={{ pointerEvents: 'none' }}> - <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={e => {}} /> + <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={emptyFunction} /> </div> {pointerEvents === 'none' ? null : ( <Popup @@ -301,7 +304,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp background={SettingsManager.userBackgroundColor} popup={ <div style={{ width: 'fit-content', height: '200px' }}> - <DatePicker open={true} dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} /> + <DatePicker open dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} /> </div> } /> @@ -329,7 +332,7 @@ export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps fieldProps.isContentActive = this.selectedFunc; return ( <div className="schemaRTFCell" style={{ fontStyle: this.selected ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}> - {this.selected ? <FormattedTextBox {...fieldProps} autoFocus={true} onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} + {this.selected ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} </div> ); } @@ -355,8 +358,8 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp checked={BoolCast(this._props.Document[this._props.fieldKey])} onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => { if ((value?.nativeEvent as any).shiftKey) { - this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); - } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); + this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); + } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); })} /> <EditableView @@ -391,7 +394,7 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC return this._props.isRowActive() && selected?.[0] === this._props.Document && selected[1] === this._props.col; } render() { - const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); + const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); const options = this._props.options?.map(facet => ({ value: facet, label: facet })); return ( <div className="schemaSelectionCell" style={{ color, textDecoration, cursor, pointerEvents }}> diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 50cf26cdb..231bac541 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -21,6 +21,7 @@ import { ImageBox } from '../nodes/ImageBox'; import { VideoBox } from '../nodes/VideoBox'; import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; + // import { InkTranscription } from '../InkTranscription'; // eslint-disable-next-line prefer-arrow-callback @@ -162,15 +163,15 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([ ['font', { checkResult: () => RichTextMenu.Instance?.fontFamily, - setDoc: () => value && RichTextMenu.Instance?.setFontFamily(value), + setDoc: () => value && RichTextMenu.Instance?.setFontField(value, 'fontFamily'), }], ['highlight', { checkResult: () => RichTextMenu.Instance?.fontHighlight, - setDoc: () => value && RichTextMenu.Instance?.setHighlight(value), + setDoc: () => value && RichTextMenu.Instance?.setFontField(value, 'fontHighlight'), }], ['fontColor', { checkResult: () => RichTextMenu.Instance?.fontColor, - setDoc: () => value && RichTextMenu.Instance?.setColor(value), + setDoc: () => value && RichTextMenu.Instance?.setFontField(value, 'fontColor'), }], ['alignment', { checkResult: () => RichTextMenu.Instance?.textAlign, @@ -182,7 +183,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh let fsize = value; if (typeof fsize === 'number') fsize = fsize.toString(); if (fsize && Number(fsize).toString() === fsize) fsize += 'px'; - RichTextMenu.Instance?.setFontSize(fsize); + RichTextMenu.Instance?.setFontField(fsize, 'fontSize'); }, }], ]); @@ -207,7 +208,7 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: (RichTextMenu.Instance?.textAlign === where)): where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered): (Doc.UserDoc().textAlign === where), - toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(editorView, editorView.dispatch): + toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle(): RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)): where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered: (Doc.UserDoc().textAlign = where); } @@ -470,7 +471,7 @@ ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) { if (checkResult) { return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc())?.fontFamily); } - if (editorView) RichTextMenu.Instance?.setFontFamily(key); + if (editorView) RichTextMenu.Instance?.setFontField(key, 'fontFamily'); else Doc.UserDoc().fontFamily = key; return undefined; }); diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 38ef08ef9..9be78a6cb 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; @@ -36,14 +38,14 @@ interface LinkMenuItemProps { // drag links and drop link targets (embedding them if needed) export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { - const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; + const draggedDocs = (specificLinks || LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; if (draggedDocs.length) { const moddrag: Doc[] = []; - for (const draggedDoc of draggedDocs) { + draggedDocs.forEach(async draggedDoc => { const doc = await Cast(draggedDoc.annotationOn, Doc); if (doc) moddrag.push(doc); - } + }); const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); dragData.canEmbed = true; @@ -108,7 +110,11 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined; if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { - setTimeout(action(() => (SettingsManager.Instance.propertiesWidth = 250))); + setTimeout( + action(() => { + SettingsManager.Instance.propertiesWidth = 250; + }) + ); } } }) @@ -166,8 +172,12 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { return ( <div className="linkMenu-item" - onPointerEnter={action(e => (this._hover = true))} - onPointerLeave={action(e => (this._hover = false))} + onPointerEnter={action(() => { + this._hover = true; + })} + onPointerLeave={action(() => { + this._hover = false; + })} style={{ fontSize: this._hover ? 'larger' : undefined, fontWeight: this._hover ? 'bold' : undefined, @@ -176,15 +186,15 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { <div className="linkMenu-item-content expand-two"> <div ref={this._drag} - className="linkMenu-name" //title="drag to view target. click to customize." + className="linkMenu-name" // title="drag to view target. click to customize." onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-item-buttons"> - <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Edit Link</div>}> + <Tooltip disableInteractive title={<div className="dash-tooltip">Edit Link</div>}> <div className="linkMenu-icon-wrapper" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="linkMenu-icon" icon="edit" size="sm" /> </div> </Tooltip> - <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Show/Hide Link</div>}> + <Tooltip disableInteractive title={<div className="dash-tooltip">Show/Hide Link</div>}> <div className="linkMenu-icon-wrapper" onPointerDown={this.onIconDown}> <FontAwesomeIcon className="linkMenu-icon" icon={destinationIcon} size="sm" /> </div> @@ -213,7 +223,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { </p> ) : null} <div className="linkMenu-title-wrapper"> - <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Follow Link</div>}> + <Tooltip disableInteractive title={<div className="dash-tooltip">Follow Link</div>}> <p className="linkMenu-destination-title"> {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} </p> @@ -223,7 +233,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { </div> <div className="linkMenu-item-buttons"> - <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Delete Link</div>}> + <Tooltip disableInteractive title={<div className="dash-tooltip">Delete Link</div>}> <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index f2681bf2e..9fb1c0fdc 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -1,15 +1,12 @@ -import { action, observable } from 'mobx'; +/* eslint-disable react/require-default-props */ import { observer } from 'mobx-react'; -import { EditorView } from 'prosemirror-view'; import * as React from 'react'; -import { emptyFunction } from '../../../Utils'; import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; import { DefaultStyleProvider } from '../StyleProvider'; -import { OpenWhere, returnEmptyDocViewList } from '../nodes/DocumentView'; -import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { returnEmptyDocViewList } from '../nodes/DocumentView'; import { SearchBox } from '../search/SearchBox'; import './LinkPopup.scss'; @@ -29,16 +26,6 @@ interface LinkPopupProps { @observer export class LinkPopup extends React.Component<LinkPopupProps> { - @observable private linkURL: string = ''; - @observable public view?: EditorView = undefined; - - // TODO: should check for valid URL - @undoBatch - makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target); - - @action - onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value); - getPWidth = () => 500; getPHeight = () => 500; @@ -65,7 +52,7 @@ export class LinkPopup extends React.Component<LinkPopupProps> { docViewPath={returnEmptyDocViewList} linkFrom={linkDoc} linkCreateAnchor={this.props.linkCreateAnchor} - linkSearch={true} + linkSearch linkCreated={this.props.linkCreated} fieldKey="data" isSelected={returnTrue} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 958d63267..6a86af6a7 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -97,7 +97,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (this.props.transition && !this.Document[TransitionTimer]) { const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]); this.Document[TransitionTimer] = setTimeout( - action(() => (this.Document[TransitionTimer] = this.Transition = undefined)), + action(() => { + this.Document[TransitionTimer] = this.Transition = undefined; + }), num ); } @@ -105,7 +107,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) { super.componentDidUpdate(prevProps); - this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); + this.WrapperKeys.forEach( + action(keys => { + (this as any)[keys.upper] = (this.props as any)[keys.lower]; + }) + ); } CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking @@ -121,6 +127,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.BackgroundColor; case StyleProp.Color: return this.Color; + default: } // prettier-ignore } return this._props.styleProvider?.(doc, props, property); @@ -129,7 +136,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static getValues(doc: Doc, time: number, fillIn: boolean = true) { return CollectionFreeFormDocumentView.animFields.reduce( (p, val) => { - p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); + p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce( + (prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), + undefined as any as number + ); return p; }, {} as { [val: string]: Opt<number> } @@ -139,7 +149,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static getStringValues(doc: Doc, time: number) { return CollectionFreeFormDocumentView.animStringFields.reduce( (p, val) => { - p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as any as string); return p; }, {} as { [val: string]: Opt<string> } @@ -179,15 +189,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { docs.forEach(doc => { if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; - if (!doc['opacity_indexed']) { + if (!doc.opacity_indexed) { // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in - doc['opacity_indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); + doc.opacity_indexed = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } - CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); - CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); - CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animFields.forEach(val => { + doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val); + }); + CollectionFreeFormDocumentView.animStringFields.forEach(val => { + doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode); + }); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { + doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode); + }); const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so - //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) + // doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); @@ -202,16 +218,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (topDoc.z) { const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y)); topDoc.z = 0; - topDoc.x = spt[0]; - topDoc.y = spt[1]; + [topDoc.x, topDoc.y] = spt; this._props.removeDocument?.(topDoc); this._props.addDocTab(topDoc, OpenWhere.inParentFromScreen); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); topDoc.z = 1; - topDoc.x = fpt[0]; - topDoc.y = fpt[1]; + [topDoc.x, topDoc.y] = fpt; } setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0); } @@ -259,6 +273,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView + // eslint-disable-next-line react/jsx-props-no-spreading {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore DataTransition={this.DataTransition} LocalRotation={this.localRotation} @@ -274,6 +289,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame); }); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 708536de0..c1aa1c699 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, makeObservable, observable, trace } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; @@ -69,7 +69,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() e, this.onPointerMove, emptyFunction, - action((e, doubleTap) => { + action((moveEv, doubleTap) => { if (doubleTap) { this._isAnyChildContentActive = true; if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); @@ -140,14 +140,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() setupMoveUpEvents( this, e, - e => { + moveEv => { const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move); de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { this.clearDoc(which); return addDocument(doc); }; de.canEmbed = true; - DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY); + DragManager.StartDocumentDrag([this._closeRef.current!], de, moveEv.clientX, moveEv.clientY); return true; }, emptyFunction, @@ -158,10 +158,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (property === StyleProp.PointerEvents) return 'none'; return this._props.styleProvider?.(doc, props, property); }; - moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); - moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); - remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); - remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); + moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); + remDoc1 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); + remDoc2 = (docs: Doc | Doc[]) => (docs instanceof Doc ? [docs] : docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); /** * Tests for whether a comparison box slot (ie, before or after) has renderable text content @@ -196,7 +196,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() _closeRef = React.createRef<HTMLDivElement>(); render() { - trace(); const clearButton = (which: string) => ( <div ref={this._closeRef} diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 2a0bc42ee..b87fead4f 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox } from '@mui/material'; import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; @@ -9,7 +10,6 @@ import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { listSpec } from '../../../../fields/Schema'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; @@ -62,9 +62,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im setupMoveUpEvents( this, e, - action(e => { + action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -95,7 +95,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { - var records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; } @@ -110,11 +110,15 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im @computed.struct get axes() { return StrListCast(this.layoutDoc._dataViz_axes); } - selectAxes = (axes: string[]) => (this.layoutDoc._dataViz_axes = new List<string>(axes)); + selectAxes = (axes: string[]) => { + this.layoutDoc._dataViz_axes = new List<string>(axes); + }; @computed.struct get titleCol() { return StrCast(this.layoutDoc._dataViz_titleCol); } - selectTitleCol = (titleCol: string) => (this.layoutDoc._dataViz_titleCol = titleCol); + selectTitleCol = (titleCol: string) => { + this.layoutDoc._dataViz_titleCol = titleCol; + }; @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { @@ -124,7 +128,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors); - Object.keys(this.layoutDoc).map(key => { + Object.keys(this.layoutDoc).forEach(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { this.layoutDoc['_' + key] = data[key]; } @@ -150,7 +154,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im annotationOn: this.Document, // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) - /*put in some options*/ + /* put in some options */ }); anchor.config_dataViz = this.dataVizView; anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; @@ -158,24 +162,21 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors); anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor; anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors); - Object.keys(this.layoutDoc).map(key => { + Object.keys(this.layoutDoc).forEach(key => { if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { anchor[key] = this.layoutDoc[key]; } }); this.addDocument(anchor); - //addAsAnnotation && this.addDocument(anchor); + // addAsAnnotation && this.addDocument(anchor); return anchor; }; createNoteAnnotation = () => { - const createFunc = undoable( - action(() => { - const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); - }), - 'create note annotation' - ); + const createFunc = undoable(() => { + this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); + }, 'create note annotation'); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); @@ -192,7 +193,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; @computed get SidebarShown() { - return this.layoutDoc._layout_showSidebar ? true : false; + return !!this.layoutDoc._layout_showSidebar; } @computed get sidebarHandle() { return ( @@ -206,7 +207,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -218,7 +219,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im setupMoveUpEvents( this, e, - (e, down, delta) => + (moveEv, down, delta) => runInAction(() => { const localDelta = this._props .ScreenToLocalTransform() @@ -246,7 +247,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im options.didMove = true; this.toggleSidebar(); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)); + }); }; @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -266,32 +269,31 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); this._disposers.datavis = reaction( () => { - if (this.layoutDoc.dataViz_schemaLive == undefined) this.layoutDoc.dataViz_schemaLive = true; + if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true; const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); - const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); - if (!keys) return; - const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); - var current: { [key: string]: string }[] = []; + const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text'); + const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]); + const current: { [key: string]: string }[] = []; children .filter(child => child) .forEach(child => { const row: { [key: string]: string } = {}; keys.forEach(key => { - var cell = child[key]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + let cell = child[key]; + if (cell && (cell as string)) cell = cell.toString().replace(/,/g, ''); row[key] = StrCast(cell); }); current.push(row); }); if (!this.layoutDoc._dataViz_schemaOG) { // makes a copy of the original table for the "live" toggle - let csvRows = []; + const csvRows = []; csvRows.push(keys.join(',')); for (let i = 0; i < children.length - 1; i++) { - let eachRow = []; + const eachRow = []; for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + let cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/,/g, ''); eachRow.push(cell); } csvRows.push(eachRow); @@ -305,19 +307,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im } const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc; const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; - const href = CsvCast(this.Document[this.fieldKey]).url.href; + const { href } = CsvCast(this.Document[this.fieldKey]).url; if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) { // sets original dataset to the var const lastRow = current.pop(); DataVizBox.datasetSchemaOG.set(href, current); current.push(lastRow!); - fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res)))); + fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.datasetSchemaOG.set(href, jsonRes)))); } return current; }, current => { if (current) { - const href = CsvCast(this.Document[this.fieldKey]).url.href; + const { href } = CsvCast(this.Document[this.fieldKey]).url; if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); } @@ -329,8 +331,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im fetchData = () => { if (!this.Document.dataViz_asSchema) { DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) // + .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes)))); } }; @@ -343,7 +345,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im records: this.records, axes: this.axes, titleCol: this.titleCol, - //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, + // width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9, margin: { top: 10, right: 25, bottom: 75, left: 45 }, @@ -351,11 +353,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; - case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} vizBox={this} />; - case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; - case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} + case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />; + case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />; + case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />; + default: } // prettier-ignore + return null; } @action @@ -367,10 +371,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this._marqueeing = [e.clientX, e.clientY]; const target = e.target as any; if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { + /* empty */ } else { // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. setTimeout( - action(() => (this._marqueeing = undefined)), + action(() => { + this._marqueeing = undefined; + }), 100 ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. @@ -405,9 +412,21 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im render() { const scale = this._props.NativeDimScaling?.() || 1; + const toggleBtn = (name: string, type: DataVizView) => ( + <Toggle + text={name} + toggleType={ToggleType.BUTTON} + type={Type.SEC} + color="black" + onClick={() => { + this.layoutDoc._dataViz = type; + }} + toggleStatus={this.layoutDoc._dataViz === type} + /> + ); return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty - <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div> + <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command (ctrl + p) to bring the data table to your canvas.</div> ) : ( <div className="dataViz-box" @@ -422,14 +441,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> - <Toggle text={' TABLE '} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz === DataVizView.TABLE} /> - <Toggle text={'LINECHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz === DataVizView.LINECHART} /> - <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz === DataVizView.HISTOGRAM} /> - <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> + {toggleBtn(' TABLE ', DataVizView.TABLE)} + {toggleBtn('LINECHART', DataVizView.LINECHART)} + {toggleBtn('HISTOGRAM', DataVizView.HISTOGRAM)} + {toggleBtn('PIE CHART', DataVizView.PIECHART)} </div> {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? ( - <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> + <div className="liveSchema-checkBox" style={{ width: this._props.width }}> <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> Display Live Updates to Canvas </div> @@ -445,7 +464,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 58cacef76..f0ffdbdcf 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -64,14 +64,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @computed get _histogramData() { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { - var ax0 = this._props.axes[0]; + const ax0 = this._props.axes[0]; if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes[1]; + const [ax0, ax1] = this._props.axes; if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } @@ -82,11 +81,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const [ax0, ax1] = this._props.axes; if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) { return ax0 + ' Histogram'; - } else return ax0 + ' by ' + ax1 + ' Histogram'; + } + return ax0 + ' by ' + ax1 + ' Histogram'; } @computed get parentViz() { @@ -112,8 +111,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { ); } - @action - restoreView = (data: Doc) => {}; + restoreView = () => {}; // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ @@ -133,36 +131,36 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? [] : validData.map((d: { [x: string]: any }) => !this.numericalXData // ? d[field] - : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') + : +d[field!].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') ); }; // outlines the bar selected / hovered over highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { - var sameAsCurrent: boolean; - var barCounter = -1; + let sameAsCurrent: boolean; + let barCounter = -1; const selected = svg.selectAll('.histogram-bar').filter((d: any) => { barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { - var showSelected = this.numericalYData - ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0] - : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0]; + let showSelected = this.numericalYData + ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0] + : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0]; if (this.numericalXData) { // calculating frequency - if (d[0] && d[1] && d[0] != d[1]) { + if (d[0] && d[1] && d[0] !== d[1]) { showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length }; } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length }; } if (changeSelectedVariables) { // for when a bar is selected - not just hovered over - sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] == this._currSelected![xAxisTitle] && showSelected[yAxisTitle] == this._currSelected![yAxisTitle] : false; + sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] === this._currSelected![xAxisTitle] && showSelected[yAxisTitle] === this._currSelected![yAxisTitle] : false; this._currSelected = sameAsCurrent ? undefined : showSelected; this.selectedData = sameAsCurrent ? undefined : d; } else this.hoverOverData = d; @@ -185,16 +183,16 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const xAxisTitle = Object.keys(dataSet[0])[0]; const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; const uniqueArr: unknown[] = [...new Set(data)]; - var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; - var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; + let numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; + let translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; if (numBins > this.maxBins) numBins = this.maxBins; const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); if (!this.numericalXData) { - var histStringDataSet: { [x: string]: unknown }[] = []; + const histStringDataSet: { [x: string]: unknown }[] = []; if (this.numericalYData) { for (let i = 0; i < dataSet.length; i++) { histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] }); @@ -204,15 +202,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); } for (let i = 0; i < data.length; i++) { - let barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); - histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; + const barData = histStringDataSet.filter(each => each[xAxisTitle] === data[i]); + histStringDataSet.filter(each => each[xAxisTitle] === data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1; } } histDataSet = histStringDataSet; } // initial graph and binning data for histogram - var svg = (this._histogramSvg = d3 + const svg = (this._histogramSvg = d3 .select(this._histogramRef.current) .append('svg') .attr('class', 'graph') @@ -220,23 +218,21 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g') .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); - var x = d3 + let x = d3 .scaleLinear() .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width]); - var histogram = d3 + const histogram = d3 .histogram() - .value(function (d) { - return d; - }) + .value(d => d) .domain([startingPoint!, endingPoint!]) .thresholds(x.ticks(numBins)); - var bins = histogram(data); - var eachRectWidth = width / bins.length; - var graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; + const bins = histogram(data); + let eachRectWidth = width / bins.length; + const graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; bins[0].x0 = graphStartingPoint; x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]); - var xAxis; + let xAxis; // more calculations based on bins // x-axis @@ -245,9 +241,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // uniqueArr.sort() histDataSet.sort(); for (let i = 0; i < data.length; i++) { - var index = 0; + let index = 0; for (let j = 0; j < uniqueArr.length; j++) { - if (uniqueArr[j] == data[i]) { + if (uniqueArr[j] === data[i]) { index = j; } } @@ -255,7 +251,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } bins.pop(); eachRectWidth = width / bins.length; - bins.forEach(d => (d.x0 = d.x0!)); + bins.forEach(d => { + d.x0 = d.x0!; + }); xAxis = d3 .axisBottom(x) .ticks(bins.length > 1 ? bins.length - 1 : 1) @@ -265,12 +263,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.domain([0, bins.length - 1]); translateXAxis = eachRectWidth / 2; } else { - var allSame = true; - for (var i = 0; i < bins.length; i++) { + let allSame = true; + for (let i = 0; i < bins.length; i++) { if (bins[i] && bins[i][0]) { - var compare = bins[i][0]; + const compare = bins[i][0]; for (let j = 1; j < bins[i].length; j++) { - if (bins[i][j] != compare) allSame = false; + if (bins[i][j] !== compare) allSame = false; } } } @@ -279,8 +277,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { eachRectWidth = width / bins.length; } else { eachRectWidth = width / (bins.length + 1); - var tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0; - var curDomain = x.domain(); + const tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0; + const curDomain = x.domain(); x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]); } @@ -288,16 +286,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.range([0, width - eachRectWidth]); } // y-axis - const maxFrequency = this.numericalYData - ? d3.max(histDataSet, function (d: any) { - return d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0; - }) - : d3.max(bins, function (d) { - return d.length; - }); - var y = d3.scaleLinear().range([height, 0]); + const maxFrequency = this.numericalYData ? + d3.max(histDataSet, (d: any) => (d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '') + .replace(/%/g, '').replace(/</g, '')) : 0)) : + d3.max(bins, d => d.length); // prettier-ignore + const y = d3.scaleLinear().range([height, 0]); y.domain([0, +maxFrequency!]); - var yAxis = d3.axisLeft(y).ticks(maxFrequency!); + const yAxis = d3.axisLeft(y).ticks(maxFrequency!); if (this.numericalYData) { const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0); yAxisCreator(svg.append('g'), width, yScale); @@ -312,18 +307,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); const onHover = action((e: any) => { this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); + // eslint-disable-next-line no-use-before-define updateHighlights(); }); - const mouseOut = action((e: any) => { + const mouseOut = action(() => { this.hoverOverData = undefined; + // eslint-disable-next-line no-use-before-define updateHighlights(); }); const updateHighlights = () => { - const hoverOverBar = this.hoverOverData; - const selectedData = this.selectedData; - svg.selectAll('rect').attr('class', function (d: any) { - return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar'; - }); + const { hoverOverData: hoverOverBar, selectedData } = this; + svg.selectAll('rect').attr('class', (d: any) => ((hoverOverBar && hoverOverBar[0] === d[0]) || (selectedData && selectedData[0] === d[0]) ? 'histogram-bar hover' : 'histogram-bar')); }; svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); @@ -333,7 +327,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .style('text-anchor', 'middle') .text(xAxisTitle); svg.append('text') - .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('transform', 'rotate(-90) translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) .attr('y', -20) .style('text-anchor', 'middle') @@ -341,7 +335,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { d3.format('.0f'); // draw bars - var selected = this.selectedData; + const selected = this.selectedData; svg.selectAll('rect') .data(bins) .enter() @@ -349,49 +343,34 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr( 'transform', this.numericalYData - ? function (d) { - const eachData = histDataSet.filter((data: { [x: string]: number }) => { - return data[xAxisTitle] == d[0]; - }); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + ? d => { + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return 'translate(' + x(d.x0!) + ',' + y(length) + ')'; } - : function (d) { - return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'; - } + : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')' ) .attr( 'height', this.numericalYData - ? function (d) { - const eachData = histDataSet.filter((data: { [x: string]: number }) => { - return data[xAxisTitle] == d[0]; - }); - const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + ? d => { + const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]); + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return height - y(length); } - : function (d) { - return height - y(d.length); - } + : d => height - y(d.length) ) .attr('width', eachRectWidth) - .attr( - 'class', - selected - ? function (d) { - return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; - } - : function (d) { - return 'histogram-bar'; - } - ) + .attr('class', selected ? d => (selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') .attr('fill', d => { - var barColor; + let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + // eslint-disable-next-line prefer-destructuring + if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); + // eslint-disable-next-line prefer-destructuring if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); @@ -401,7 +380,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @action changeSelectedColor = (color: string) => { this.curBarSelected.attr('fill', color); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); @@ -410,22 +389,24 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @action eraseSelectedColor = () => { this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); }; updateBarColors = () => { - var svg = this._histogramSvg; + const svg = this._histogramSvg; if (svg) svg.selectAll('rect').attr('fill', (d: any) => { - var barColor; + let barColor; const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + // eslint-disable-next-line prefer-destructuring + if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1]; else { const range = StrCast(each[0]).split(' to '); + // eslint-disable-next-line prefer-destructuring if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); @@ -436,38 +417,41 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { render() { this.updateBarColors(); this._histogramData; - var curSelectedBarName = ''; - var titleAccessor: any = 'dataViz_histogram_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let curSelectedBarName = ''; + let titleAccessor: any = 'dataViz_histogram_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); - var selected = 'none'; + let selected = 'none'; if (this._currSelected) { - curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); selected = '{ '; - Object.keys(this._currSelected).forEach(key => + Object.keys(this._currSelected).forEach(key => { key // ? (selected += key + ': ' + this._currSelected[key] + ', ') - : '' - ); + : ''; + }); selected = selected.substring(0, selected.length - 2) + ' }'; - if (this._props.titleCol != '' && (!this._currSelected['frequency'] || this._currSelected['frequency'] < 10)) { + if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) { selected += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - if (this._currSelected[this._props.axes[0]] == each[this._props.axes[0]]) { + if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) { if (this._props.axes[1]) { - if (this._currSelected[this._props.axes[1]] == each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; + if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; } else selected += each[this._props.titleCol] + ', '; } }); selected = selected.slice(0, -1).slice(0, -1); } } - var selectedBarColor; - var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); + let selectedBarColor; + const barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); + barColors.forEach(each => { + // eslint-disable-next-line prefer-destructuring + each[0] === curSelectedBarName && (selectedBarColor = each[1]); + }); if (this._histogramData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( @@ -476,45 +460,51 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> <ColorPicker - tooltip={'Change Default Bar Color'} + tooltip="Change Default Bar Color" type={Type.SEC} icon={<FaFillDrip />} selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)} - setFinalColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setFinalColor={undoable(color => { + this._props.layoutDoc.dataViz_histogram_defaultColor = color; + }, 'Change Default Bar Color')} + setSelectedColor={undoable(color => { + this._props.layoutDoc.dataViz_histogram_defaultColor = color; + }, 'Change Default Bar Color')} size={Size.XSMALL} /> </div> <div ref={this._histogramRef} /> - {selected != 'none' ? ( - <div className={'selected-data'}> + {selected !== 'none' ? ( + <div className="selected-data"> Selected: {selected} <ColorPicker - tooltip={'Change Bar Color'} + tooltip="Change Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={selectedBarColor ? selectedBarColor : this.curBarSelected.attr('fill')} + selectedColor={selectedBarColor || this.curBarSelected.attr('fill')} setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} size={Size.XSMALL} /> <IconButton - icon={<FontAwesomeIcon icon={'eraser'} />} + icon={<FontAwesomeIcon icon="eraser" />} size={Size.XSMALL} - color={'black'} + color="black" type={Type.SEC} - tooltip={'Revert to the default bar color'} + tooltip="Revert to the default bar color" onClick={undoable( action(() => this.eraseSelectedColor()), 'Change Selected Bar Color' @@ -524,7 +514,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select a column to graph'}</span> + <span className="chart-container"> first use table view to select a column to graph</span> ); } // when it is a brushed table and the incoming table doesn't have any rows selected diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index c667a15de..8105adf1e 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -3,7 +3,7 @@ import * as d3 from 'd3'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, DocListCast, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; @@ -63,7 +63,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get _lineChartData() { - var guids = StrListCast(this._props.layoutDoc.dataViz_rowIds); if (this._props.axes.length <= 1) return []; return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @@ -103,7 +102,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { ); this._disposers.annos = reaction( () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_annotations']), - annotations => { + (/* annotations */) => { // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way // could be blue colored to make it look like anchor // this.drawAnnotations() @@ -176,7 +175,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ // - title: 'line doc selection' + this._currSelected?.x, + title: 'line doc selection' + (this._currSelected?.x ?? ''), }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; @@ -192,11 +191,12 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const ax0 = this._props.axes[0]; + const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; - } else return ax1 + ' by ' + ax0 + ' Line Chart'; + } + return ax1 + ' by ' + ax0 + ' Line Chart'; } setupTooltip() { @@ -216,9 +216,11 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @action setCurrSelected(x?: number, y?: number) { // TODO: nda - get rid of svg element in the list? - if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; + if (this._currSelected && this._currSelected.x === x && this._currSelected.y === y) this._currSelected = undefined; else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this._props.records.forEach(record => record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true)); + this._props.records.forEach(record => { + record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true); + }); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -242,13 +244,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); - var { xMin, xMax, yMin, yMax } = rangeVals; + let { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } // adding svg - const margin = this._props.margin; + const { margin } = this._props; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') @@ -258,15 +260,15 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); - var validSecondData; + let validSecondData; if (this._props.axes.length > 2) { // for when there are 2 lines on the chart - var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + const next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); validSecondData = next.filter(d => { if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false; return true; }); - var secondDataRange = minMaxRange([validSecondData]); + const secondDataRange = minMaxRange([validSecondData]); if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax; if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax; if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin; @@ -290,45 +292,31 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { svg.append('path').attr('stroke', 'red'); // legend - var color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); + const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); svg.selectAll('mydots') .data([this._props.axes[1], this._props.axes[2]]) .enter() .append('circle') .attr('cx', 5) - .attr('cy', function (d, i) { - return -30 + i * 15; - }) + .attr('cy', (d, i) => -30 + i * 15) .attr('r', 7) - .style('fill', function (d) { - return color(d); - }); + .style('fill', d => color(d)); svg.selectAll('mylabels') .data([this._props.axes[1], this._props.axes[2]]) .enter() .append('text') .attr('x', 25) - .attr('y', function (d, i) { - return -30 + i * 15; - }) - .style('fill', function (d) { - return color(d); - }) - .text(function (d) { - return d; - }) + .attr('y', (d, i) => -30 + i * 15) + .style('fill', d => color(d)) + .text(d => d) .attr('text-anchor', 'left') .style('alignment-baseline', 'middle'); } // get valid data points const data = dataSet[0]; - var validData = data.filter(d => { - Object.keys(data[0]).map(key => { - if (!d[key] || isNaN(d[key])) return false; - }); - return true; - }); + const keys = Object.keys(data[0]); + const validData = data.filter(d => !keys.some(key => isNaN(d[key]))); // draw the plot line drawLine(svg.append('path'), validData, lineGen, false); @@ -355,7 +343,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; // find .circle-d1 with data-x = d0.x and data-y = d0.y - const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); + svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); this.setCurrSelected(d0.x, d0.y); this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); @@ -378,7 +366,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .style('text-anchor', 'middle') .text(this._props.axes[0]); svg.append('text') - .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('transform', 'rotate(-90) translate(0, -10)') .attr('x', -(height / 2)) .attr('y', -30) .attr('height', 20) @@ -404,57 +392,60 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } render() { - var titleAccessor: any = 'dataViz_lineChart_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let titleAccessor: any = 'dataViz_lineChart_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; - var selectedTitle = ''; + let selectedTitle = ''; if (this._currSelected && this._props.titleCol) { selectedTitle += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - var mapThisEntry = false; - if (this._currSelected.x == each[this._props.axes[0]] && this._currSelected.y == each[this._props.axes[1]]) mapThisEntry = true; - else if (this._currSelected.y == each[this._props.axes[0]] && this._currSelected.x == each[this._props.axes[1]]) mapThisEntry = true; + let mapThisEntry = false; + if (this._currSelected.x === each[this._props.axes[0]] && this._currSelected.y === each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected.y === each[this._props.axes[0]] && this._currSelected.x === each[this._props.axes[1]]) mapThisEntry = true; if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', '; }); selectedTitle = selectedTitle.slice(0, -1).slice(0, -1); } - if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { + if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length === 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> </div> <div ref={this._lineChartRef} /> - {selectedPt != 'none' ? ( - <div className={'selected-data'}> + {selectedPt !== 'none' ? ( + <div className="selected-data"> {`Selected: ${selectedPt}`} {`${selectedTitle}`} <Button - onClick={e => { + onClick={() => { this._props.vizBox.sidebarBtnDown; this._props.vizBox.sidebarAddDocument; - }}></Button> + }} + /> </div> ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span> - ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + <span className="chart-container"> first use table view to select two numerical axes to plot</span> ); + } + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 15959c61d..55c221046 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { Button, Type } from 'browndash-components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -13,7 +15,9 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; + const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore + interface TableBoxProps { Document: Doc; layoutDoc: Doc; @@ -71,7 +75,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } @computed get columns() { - return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; + return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header !== '' && header !== undefined) : []; } // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table @@ -108,13 +112,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); else highlited?.push(rowId); if (!selected?.includes(rowId)) selected?.push(rowId); - } else { + } else if (selected?.includes(rowId)) { // selecting a row - if (selected?.includes(rowId)) { - if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - selected.splice(selected.indexOf(rowId), 1); - } else selected?.push(rowId); - } + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + selected.splice(selected.indexOf(rowId), 1); + } else selected?.push(rowId); e.stopPropagation(); }; @@ -124,7 +126,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { setupMoveUpEvents( {}, e, - e => { + moveEv => { // dragging off a column to create a brushed DataVizBox const sourceAnchorCreator = () => this._props.docView?.()!.Document!; const targetCreator = (annotationOn: Doc | undefined) => { @@ -138,13 +140,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors); return embedding; }; - if (this._props.docView?.() && !ClientUtils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.linkDocument.link_displayLine = true; - e.linkDocument.link_matchEmbeddings = true; - e.linkDocument.link_displayArrow = true; + if (this._props.docView?.() && !ClientUtils.isClick(moveEv.clientX, moveEv.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: completeEv => { + if (!completeEv.aborted && completeEv.annoDragData && completeEv.annoDragData.linkSourceDoc && completeEv.annoDragData.dropDocument && completeEv.linkDocument) { + completeEv.linkDocument.link_displayLine = true; + completeEv.linkDocument.link_matchEmbeddings = true; + completeEv.linkDocument.link_displayArrow = true; // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document; // e.annoDragData.linkSourceDoc.followLinkZoom = false; } @@ -155,9 +157,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return false; }, emptyFunction, - action(e => { - if (e.shiftKey) { - if (this._props.titleCol == col) this._props.titleCol = ''; + action(clickEv => { + if (clickEv.shiftKey) { + if (this._props.titleCol === col) this._props.titleCol = ''; else this._props.titleCol = col; this._props.selectTitleCol(this._props.titleCol); } else { @@ -185,8 +187,22 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } }}> <div className="selectAll-buttons"> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + })} + text="Select All" + type={Type.SEC} + color="black" + /> + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(); + })} + text="Deselect All" + type={Type.SEC} + color="black" + /> </div> <div className={`tableBox-container ${this.columns[0]}`} @@ -225,7 +241,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ? 'darkgreen' : this._props.axes.length > 2 && this._props.axes.lastElement() === col ? 'darkred' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) ? 'darkblue' : undefined, background: @@ -233,7 +249,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ? '#E3fbdb' : this._props.axes.length > 2 && this._props.axes.lastElement() === col ? '#Fbdbdb' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) ? '#c6ebf7' : undefined, // blue: #ADD8E6 @@ -260,11 +276,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - var colSelected = false; - if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col; - else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col; - else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col; - if (this._props.titleCol == col) colSelected = true; + let colSelected = false; + if (this._props.axes.length > 2) colSelected = this._props.axes[0] === col || this._props.axes[1] === col || this._props.axes[2] === col; + else if (this._props.axes.length > 1) colSelected = this._props.axes[0] === col || this._props.axes[1] === col; + else if (this._props.axes.length > 0) colSelected = this._props.axes[0] === col; + if (this._props.titleCol === col) colSelected = true; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> <div className="tableBox-cell">{this._props.records[rowId][col]}</div> @@ -279,10 +295,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { </div> </div> ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> - ); + } + return ( + // when it is a brushed table and the incoming table doesn't have any rows selected + <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> + ); } } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 518158a7f..832e18b68 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -18,7 +19,7 @@ import { CollectionView } from '../collections/CollectionView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; -import { PresElementBox } from '../nodes/trails/PresElementBox'; +import { PresElementBox } from './trails/PresElementBox'; import { SearchBox } from '../search/SearchBox'; import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; import { AudioBox } from './AudioBox'; @@ -71,8 +72,8 @@ interface HTMLtagProps { children?: JSX.Element[]; } -//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" -//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%' +// "<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" +// "<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%' // transform='rotate({2*this.x+this.y}deg)' // onClick = { this.bannerColor = this.bannerColor === 'red' ? 'green' : 'red' } > // <ImageBox {...props} fieldKey={'data'}/> @@ -85,7 +86,7 @@ interface HTMLtagProps { // </HTMLdiv>" @observer export class HTMLtag extends React.Component<HTMLtagProps> { - click = (e: React.MouseEvent) => { + click = () => { const clickScript = (this.props as any).onClick as Opt<ScriptField>; clickScript?.script.run({ this: this.props.Document, self: this.props.Document, scale: this.props.scaling }); }; @@ -96,11 +97,10 @@ export class HTMLtag extends React.Component<HTMLtagProps> { render() { const style: { [key: string]: any } = {}; const divKeys = OmitKeys(this.props, ['children', 'dragStarting', 'dragEnding', 'htmltag', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit; - const replacer = (match: any, expr: string, offset: any, string: any) => { + const replacer = (match: any, expr: string) => // bcz: this executes a script to convert a property expression string: { script } into a value - return (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || ''; - }; - Object.keys(divKeys).map((prop: string) => { + (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.Document, this: this.props.Document, scale: this.props.scaling }).result as string) || ''; + Object.keys(divKeys).forEach((prop: string) => { const p = (this.props as any)[prop] as string; style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer); }); @@ -158,7 +158,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte 'dontCenter', 'DataTransition', 'contextMenuItems', - //'onClick', // don't need to omit this since it will be set + // 'onClick', // don't need to omit this since it will be set 'onDoubleClickScript', 'onPointerDownScript', 'onPointerUpScript', @@ -187,21 +187,16 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte let layoutFrame = this.layout; // replace code content with a script >{content}< as in <HTMLdiv>{this.title}</HTMLdiv> - const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => { - return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix; - }; + const replacer = (match: any, prefix: string, expr: string, postfix: string) => prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this._props.Document }).result as string) || '') + postfix; layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer); // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> - const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; - }; + const replacer2 = (match: any, p1: string) => `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); // replace /HTML<tag> with </HTMLdiv> as in: </HTMLdiv> becomes </HTMLtag> - const replacer3 = (match: any, p1: string, offset: any, string: any) => { - return `</HTMLtag`; - }; + const replacer3 = (/* match: any, p1: string, offset: any, string: any */) => `</HTMLtag`; + layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3); // add onClick function to props @@ -271,7 +266,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte }} bindings={bindings} jsx={layoutFrame} - showWarnings={true} + showWarnings onError={(test: any) => { console.log('DocumentContentsView:' + test, bindings, layoutFrame); }} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index d378082f8..a029b3761 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; @@ -26,20 +28,22 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; OnHover?: boolean; - StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) + StartLink?: boolean; // whether the link HAS been started (i.e. now needs to be completed) ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it hideCount?: () => boolean; } export class DocButtonState { - @observable public StartLink: Doc | undefined = undefined; //origin's Doc, if defined + @observable public StartLink: Doc | undefined = undefined; // origin's Doc, if defined @observable public StartLinkView: DocumentView | undefined = undefined; @observable public AnnotationId: string | undefined = undefined; @observable public AnnotationUri: string | undefined = undefined; @observable public LinkEditorDocView: DocumentView | undefined = undefined; + // eslint-disable-next-line no-use-before-define public static _instance: DocButtonState | undefined; public static get Instance() { + // eslint-disable-next-line no-return-assign return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState()); } constructor() { @@ -50,7 +54,7 @@ export class DocButtonState { export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksButtonProps> { private _linkButton = React.createRef<HTMLDivElement>(); public static get StartLink() { return DocButtonState.Instance.StartLink; } // prettier-ignore - public static set StartLink(value) { runInAction(() => (DocButtonState.Instance.StartLink = value)); } // prettier-ignore + public static set StartLink(value) { runInAction(() => {DocButtonState.Instance.StartLink = value}); } // prettier-ignore @observable public static StartLinkView: DocumentView | undefined = undefined; @observable public static AnnotationId: string | undefined = undefined; @observable public static AnnotationUri: string | undefined = undefined; @@ -93,7 +97,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB }), undefined, undefined, - action(() => (DocButtonState.Instance.LinkEditorDocView = this._props.View)) + action(() => { + DocButtonState.Instance.LinkEditorDocView = this._props.View; + }) ); }; @@ -105,7 +111,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB emptyFunction, action((e, doubleTap) => { if (doubleTap && this._props.InMenu && this._props.StartLink) { - //action(() => Doc.BrushDoc(this._props.View.Document)); + // action(() => Doc.BrushDoc(this._props.View.Document)); if (DocumentLinksButton.StartLink === this._props.View.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -119,7 +125,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB }; @undoBatch - onLinkClick = (e: React.MouseEvent): void => { + onLinkClick = (): void => { if (this._props.InMenu && this._props.StartLink) { DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; @@ -127,7 +133,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { - //if this LinkButton's Document is undefined + // if this LinkButton's Document is undefined DocumentLinksButton.StartLink = this._props.View.Document; DocumentLinksButton.StartLinkView = this._props.View; } @@ -154,7 +160,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB DocumentLinksButton.AnnotationUri = undefined; // !this._props.StartLink } else if (startLink !== endLink) { + // eslint-disable-next-line no-param-reassign endLink = endLinkView?.ComponentView?.getAnchor?.(true, pinProps) || endLink; + // eslint-disable-next-line no-param-reassign startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink; const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); @@ -193,7 +201,9 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB } setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2500 ); } @@ -243,13 +253,13 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB showLinkCount(this._props.OnHover, this._props.Bottom) ) : ( <div className="documentLinksButton-menu"> - {this._props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + {this._props.StartLink ? ( // if link has been started from current node, then set behavior of link button to deactivate linking when clicked again <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} - {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( //if the origin node is not this node - <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}> + {!this._props.StartLink && DocumentLinksButton.StartLink !== this._props.View.Document ? ( // if the origin node is not this node + <div className="documentLinksButton-endLink" ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}> <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} @@ -263,7 +273,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB const buttonTitle = 'Tap to view links; double tap to open link collection'; const title = this._props.ShowCount ? buttonTitle : menuTitle; - //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu + // render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu return !Array.from(this.filteredLinks).length && !this._props.AlwaysOn ? null : ( <div className="documentLinksButton-wrapper" diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 62a3e2467..5962cd09f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -118,7 +118,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without * needing to know about/reference MainView */ - public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; + public static addDocTabFunc: (doc: Doc | Doc[], location: OpenWhere) => boolean = returnFalse; private _disposers: { [name: string]: IReactionDisposer } = {}; private _doubleClickTimeout: NodeJS.Timeout | undefined; diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 14454ff61..5c91d5aca 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react/no-unused-prop-types */ +/* eslint-disable react/require-default-props */ import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -31,6 +33,7 @@ export interface FocusViewOptions { easeFunc?: 'linear' | 'ease'; // transition method for scrolling } export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>; +// eslint-disable-next-line no-use-before-define export type StyleProviderFuncType = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => any; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -73,13 +76,14 @@ export interface FieldViewSharedProps { onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; onBrowseClickScript?: () => ScriptField | undefined; + // eslint-disable-next-line no-use-before-define onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; layout_fitWidth?: (doc: Doc) => boolean | undefined; searchFilterDocs: () => Doc[]; layout_showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected?: () => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: OpenWhere) => boolean; + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; @@ -120,7 +124,7 @@ export interface FieldViewProps extends FieldViewSharedProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; // e.g., "<ImageBox {...props} fieldKey={'data'} />" } @computed get fieldval() { return this.props.Document[this.props.fieldKey]; @@ -135,6 +139,6 @@ export class FieldView extends React.Component<FieldViewProps> { if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>; if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>; if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>; - return <p> {'Waiting for server...'} </p>; + return <p> Waiting for server... </p>; } } diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 70fc63115..79738c452 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,13 +1,14 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, Utils } from '../../../../Utils'; -import { ClientUtils, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -34,7 +35,7 @@ export enum ButtonType { NumberSliderButton = 'numSliderBtn', NumberDropdownButton = 'numDropdownBtn', NumberInlineButton = 'numInlineBtn', - EditableText = 'editableText', + EditText = 'editableText', } export interface ButtonProps extends FieldViewProps { @@ -83,7 +84,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { if (iconFalse) { icon = StrCast(this.dataDoc[this.fieldKey ?? 'iconFalse'] ?? this.dataDoc.icon, 'user') as any; if (icon) return <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; - else return null; + return null; } icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; @@ -109,7 +110,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * - Color button * - Dropdown list * - Number button - **/ + * */ _batch: UndoManager.Batch | undefined = undefined; /** @@ -118,17 +119,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { @computed get numberDropdown() { let type: NumberDropdownType; switch (this.type) { - case ButtonType.NumberDropdownButton: - type = 'dropdown'; - break; - case ButtonType.NumberInlineButton: - type = 'input'; - break; + case ButtonType.NumberDropdownButton: type = 'dropdown'; break; + case ButtonType.NumberInlineButton: type = 'input'; break; case ButtonType.NumberSliderButton: - default: - type = 'slider'; + default: type = 'slider'; break; - } + } // prettier-ignore const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, self: this.Document, value, _readOnly_: value === undefined }); const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); // Script for checking the outcome of the toggle @@ -155,12 +151,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { setupMoveUpEvents( this, e, - (e: PointerEvent) => { - return ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result; - }, + () => ScriptCast(this.Document.onDragScript)?.script.run({ this: this.Document, self: this.Document, value: { doc: value, e } }).result, emptyFunction, emptyFunction - ); + ); // prettier-ignore return false; }; @@ -189,7 +183,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { } return ( <Popup - icon={<FontAwesomeIcon size={'1x'} icon={icon} />} + icon={<FontAwesomeIcon size="1x" icon={icon} />} text={text} type={Type.TERT} color={SettingsManager.userColor} @@ -275,8 +269,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { // Determine the type of toggle button const tooltip: string = StrCast(this.Document.toolTip); - const script = ScriptCast(this.Document.onClick); - const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; + // const script = ScriptCast(this.Document.onClick); + // const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const items = DocListCast(this.dataDoc.data); @@ -313,7 +307,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); - const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); + // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); return ( <Toggle @@ -323,7 +317,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} - //background={SettingsManager.userBackgroundColor} + // background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={e => @@ -332,10 +326,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { e, returnTrue, emptyFunction, - action((e, doubleTap) => { + action((clickEv, doubleTap) => { (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); - this._hackToRecompute = this._hackToRecompute + 1; + this._hackToRecompute += 1; }) ) } @@ -348,27 +342,22 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { */ @computed get defaultButton() { const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); - const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); const tooltip: string = StrCast(this.Document.toolTip); return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />; } @computed get editableText() { - // Script for running the toggle const script = ScriptCast(this.Document.script); - // Function to run the script const checkResult = script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result; - const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result; - - return <EditableText editing={false} setEditing={(editing: boolean) => {}} />; + const setValue = (value: string): boolean => script?.script.run({ this: this.Document, self: this.Document, value, _readOnly_: false }).result; return ( <div className="menuButton editableText"> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="lock" /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ this: this.Document, self: this.Document, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine contents={checkResult} /> </div> </div> ); @@ -384,7 +373,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { case ButtonType.NumberDropdownButton: case ButtonType.NumberInlineButton: case ButtonType.NumberSliderButton: return this.numberDropdown; - case ButtonType.EditableText: return this.editableText; + case ButtonType.EditText: return this.editableText; case ButtonType.DropdownList: return this.dropdownListButton; case ButtonType.ColorButton: return this.colorButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; @@ -395,6 +384,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + default: } return this.defaultButton; }; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 231300a65..90b4a6740 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -22,7 +22,7 @@ import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; -import { ContextMenu } from '../../views/ContextMenu'; +import { ContextMenu } from '../ContextMenu'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; @@ -428,9 +428,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( this, e, - action(e => { + action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]); + this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -476,6 +476,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }}> <CollectionFreeFormView ref={this._ffref} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index c3afc198d..f96dd2b76 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { Tooltip } from '@mui/material'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -63,7 +64,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { render() { // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; let protoCount = 0; - let doc = this._props.doc; + let { doc } = this._props; while (doc) { if (Object.keys(doc).includes(this._props.keyName)) { break; @@ -77,10 +78,18 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( - <tr className={this._props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> + <tr + className={this._props.rowStyle} + onPointerEnter={action(() => { + this.isPointerOver = true; + })} + onPointerLeave={action(() => { + this.isPointerOver = false; + })}> <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button + type="button" style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index d1c8c62ed..80ece7cc8 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -13,6 +13,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { PinProps, ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; +// eslint-disable-next-line import/extensions import BigText from './LabelBigText'; import './LabelBox.scss'; import { PresBox } from './trails'; @@ -23,7 +24,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { return FieldView.LayoutString(LabelBox, fieldKey); } public static LayoutStringWithTitle(fieldStr: string, label?: string) { - return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; // e.g., "<ImageBox {...props} fieldKey={"data} />" } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; @@ -54,14 +55,16 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const funcs: ContextMenuProps[] = []; !Doc.noviceMode && funcs.push({ description: 'Clear Script Params', event: () => { const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); - params?.map(p => (this.paramsDoc[p] = undefined)); + params?.forEach(p => { + this.paramsDoc[p] = undefined; + }); }, icon: 'trash', }); @@ -71,7 +74,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { - const docDragData = de.complete.docDragData; + const { docDragData } = de.complete; const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); if (docDragData && missingParams?.includes((e.target as any).textContent)) { @@ -131,7 +134,10 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { }; this._timeout = undefined; if (!r) return params; - if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r))); + if (!r.offsetHeight || !r.offsetWidth) { + this._timeout = setTimeout(() => this.fitTextToBox(r)); + return this._timeout; + } const parent = r.parentNode; const parentStyle = parent.style; parentStyle.display = ''; @@ -154,8 +160,13 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="labelBox-outerDiv" - onMouseLeave={action(() => (this._mouseOver = false))} - onMouseOver={action(() => (this._mouseOver = true))} + onMouseLeave={action(() => { + this._mouseOver = false; + })} + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + onMouseOver={action(() => { + this._mouseOver = true; + })} ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) }}> diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 0155defb7..bff6d53da 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -15,7 +15,9 @@ import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; import { LinkInfo } from './LinkDocPreview'; + const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore + @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -48,7 +50,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { else this._props.select(false); }); }; - onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { + onPointerMove = action((e: PointerEvent) => { const cdiv = this._ref?.current?.parentElement; if (!this._isOpen && cdiv) { const bounds = cdiv.getBoundingClientRect(); @@ -68,7 +70,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; }); - specificContextMenu = (e: React.MouseEvent): void => {}; + specificContextMenu = (): void => {}; render() { TraceMobx(); diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 7855f8fe8..822485b8d 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -55,7 +55,6 @@ import { MarkerIcons } from './MarkerIcons'; const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; - const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; type PopupInfo = { @@ -112,13 +111,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; // this list contains pushpins and configs - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } //prettier-ignore - @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } //prettier-ignore - @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } //prettier-ignore - @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } //prettier-ignore - @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } //prettier-ignore - @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } //prettier-ignore - @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } //prettier-ignore + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore + @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore + @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore + @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @@ -259,7 +258,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } }); } - }); //add to annotation list + }); // add to annotation list return this.addDocument(doc, sidebarKey); // add to sidebar list }; @@ -326,7 +325,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -390,7 +389,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + sidebarMove = (e: PointerEvent) => { const bounds = this._ref.current!.getBoundingClientRect(); this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -654,7 +653,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem console.error(features); if (features && features.length > 0 && features[0].properties && features[0].geometry) { const geometry = features[0].geometry as LineString; - const routeTitle: string = features[0].properties['routeTitle']; + const { routeTitle } = features[0].properties; const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle); this.deselectPinOrRoute(); // TODO: Also deselect route if selected if (routeDoc) { @@ -699,7 +698,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem */ handleMapDblClick = async (e: MapLayerMouseEvent) => { e.preventDefault(); - const lngLat: LngLat = e.lngLat; + const { lngLat }: LngLat = e; const longitude: number = lngLat.lng; const latitude: number = lngLat.lat; diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index e857ef722..189e2105d 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { MapProvider, Map as MapboxMap } from 'react-map-gl'; import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, LinkedTo, Opt } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; @@ -15,7 +15,6 @@ import { DocUtils, Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { LinkManager } from '../../../util/LinkManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { PinProps, ViewBoxAnnotatableComponent } from '../../DocComponent'; @@ -25,9 +24,9 @@ import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; import { MapAnchorMenu } from '../MapBox/MapAnchorMenu'; +import '../MapBox/MapBox.scss'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PresBox } from '../trails'; -import './MapBox.scss'; /** * MapBox architecture: @@ -43,7 +42,6 @@ import './MapBox.scss'; */ const mapboxApiKey = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg'; -const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -89,7 +87,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } @computed get SidebarShown() { - return this.layoutDoc._layout_showSidebar ? true : false; + return !!this.layoutDoc._layout_showSidebar; } @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); @@ -139,14 +137,18 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> } }); } - }); //add to annotation list + }); // add to annotation list return this.addDocument(doc, sidebarKey); // add to sidebar list }; removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { const docs = doc instanceof Doc ? [doc] : doc; - this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined)); + this.allAnnotations + .filter(anno => docs.includes(DocCast(anno.mapPin))) + .forEach(anno => { + anno.mapPin = undefined; + }); return this.removeDocument(doc, annotationKey, undefined); }; @@ -206,7 +208,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -605,7 +607,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); this.map_docToPinMap.delete(pin); const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(pin)); this._bingMap.current.entities.push(newpin); this.map_docToPinMap.set(pin, newpin); }; @@ -637,7 +639,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> () => this.allAnnotations.map(doc => doc[Highlight]), () => { const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); - allConfigPins.forEach(({ doc, pushpin }) => { + allConfigPins.forEach(({ pushpin }) => { if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { this.recolorPin(pushpin); this.map_pinHighlighted.delete(pushpin); @@ -736,7 +738,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> return null; } - const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div @@ -746,15 +747,11 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.IsDragging ? null : renderAnnotations()} - <div className="mapBox-searchbar"> <EditableText // editing setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} - onEnter={e => this.bingSearch()} + onEnter={() => this.bingSearch()} placeholder={this.bingSearchBarContents || 'enter city/zip/...'} textAlign="center" /> @@ -792,7 +789,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> ? null : this.allAnnotations .filter(anno => !anno.layout_unrendered) - .map((pushpin, i) => ( + .map(pushpin => ( <DocumentView key={pushpin[Id]} // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8140c0ca7..cc897aaef 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -26,7 +28,7 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { PinProps, ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { Colors } from '../global/globalEnums'; -import { CreateImage } from '../nodes/WebBoxRenderer'; +import { CreateImage } from './WebBoxRenderer'; import { PDFViewer } from '../pdf/PDFViewer'; import { SidebarAnnos } from '../SidebarAnnos'; import { DocumentView, OpenWhere } from './DocumentView'; @@ -67,8 +69,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); if (this.pdfUrl) { - if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href))); - else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf))); + if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) + runInAction(() => { + this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href); + }); + else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) + PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then( + action((pdf: any) => { + this._pdf = pdf; + }) + ); } } @@ -86,7 +96,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element - img.src = canvas.toDataURL(); //image sourcez + img.src = canvas.toDataURL(); // image sourcez img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; @@ -97,7 +107,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; crop = (region: Doc | undefined, addCrop?: boolean) => { - if (!region) return; + if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); const regionData = region[DocData]; regionData.lockedPosition = true; @@ -112,11 +122,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); - const anchx = NumCast(cropping.x); - const anchy = NumCast(cropping.y); + // const anchx = NumCast(cropping.x); + // const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width) * (this._props.NativeDimScaling?.() || 1); const anchh = NumCast(cropping._height) * (this._props.NativeDimScaling?.() || 1); - const viewScale = 1; + // const viewScale = 1; cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); @@ -157,7 +167,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ) ); }) - .catch(function (error: any) { + .catch((error: any) => { console.error('oops, something went wrong!', error); }); @@ -245,7 +255,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } const docAnchor = () => Docs.Create.ConfigDocument({ - title: StrCast(this.Document.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), + title: StrCast(this.Document.title + '@' + (NumCast(this.layoutDoc._layout_scrollTop) ?? 0).toFixed(0)), annotationOn: this.Document, }); const visibleAnchor = this._pdfViewer?._getAnchor?.(this._pdfViewer.savedAnnotations(), true); @@ -288,7 +298,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this._props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); return true; }; - public gotoPage = (p: number) => (this.Document._layout_curPage = p); + public gotoPage = (p: number) => { + this.Document._layout_curPage = p; + }; @undoBatch onKeyDown = action((e: KeyboardEvent) => { @@ -300,6 +312,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem case 'PageUp': processed = this.backPage(); break; + default: } if (processed) { e.stopImmediatePropagation(); @@ -315,7 +328,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._initialScrollTarget = undefined; } }; - searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + }; // adding external documents; to sidebar key // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation") @@ -371,11 +386,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem settingsPanel() { const pageBtns = ( <> - <button className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}> - <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-left'} size="sm" /> + <button type="button" className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}> + <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-left" size="sm" /> </button> - <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}> - <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-right'} size="sm" /> + <button type="button" className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}> + <FontAwesomeIcon style={{ color: 'white' }} icon="arrow-right" size="sm" /> </button> </> ); @@ -388,7 +403,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="pdfBox-overlayButton" title={searchTitle} /> + <button type="button" className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" @@ -399,17 +414,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey); }} /> - <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <button type="button" className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> - <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}> - <FontAwesomeIcon icon={'arrow-up'} size="lg" /> + <button type="button" className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}> + <FontAwesomeIcon icon="arrow-up" size="lg" /> </button> - <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}> - <FontAwesomeIcon icon={'arrow-down'} size="lg" /> + <button type="button" className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}> + <FontAwesomeIcon icon="arrow-down" size="lg" /> </button> </div> <button + type="button" className="pdfBox-overlayButton" title={searchTitle} onClick={action(() => { @@ -426,9 +442,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }} - onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))} + onChange={e => { + this.Document._layout_curPage = Number(e.currentTarget.value); + }} onKeyDown={e => e.stopPropagation()} - onClick={action(() => (this._pageControls = !this._pageControls))} + onClick={action(() => { + this._pageControls = !this._pageControls; + })} /> {this._pageControls ? pageBtns : null} </div> @@ -443,14 +463,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return PDFBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); - specificContextMenu = (e: React.MouseEvent): void => { + toggleSidebarType = () => { + this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform; + }; + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; !Doc.noviceMode && optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); - //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); + // optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; @@ -472,7 +494,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._show_sidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._show_sidebar); } @computed get sidebarHandle() { return ( @@ -486,7 +508,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -517,6 +539,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} @@ -532,6 +555,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ) : ( <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} // override setContentView to do nothing NativeWidth={this.sidebarNativeWidthFunc} @@ -542,7 +566,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem yPadding={0} viewField={this.SidebarKey} isAnnotationOverlay={false} - originTopLeft={true} + originTopLeft isAnyChildContentActive={this.isAnyChildContentActive} select={emptyFunction} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -551,7 +575,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} renderDepth={this._props.renderDepth + 1} - noSidebar={true} + noSidebar fieldKey={this.SidebarKey} /> </div> @@ -585,6 +609,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem top: 0, }}> <PDFViewer + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} pdfBox={this} sidebarAddDoc={this.sidebarAddDocument} @@ -617,10 +642,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const pdfView = !this._pdf ? null : this.renderPdfView; const href = this.pdfUrl?.url.href; if (!pdfView && href) { - if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href)))); + if (PDFBox.pdfcache.get(href)) + setTimeout( + action(() => { + this._pdf = PDFBox.pdfcache.get(href); + }) + ); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf)))); + PDFBox.pdfpromise.get(href)?.then( + action((pdf: any) => { + PDFBox.pdfcache.set(href, (this._pdf = pdf)); + }) + ); } } return pdfView ?? this.renderTitleBox; diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 40199cce1..7d123d90c 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -47,7 +47,9 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable videoDuration: number | undefined = undefined; @action - setVideoDuration = (duration: number) => (this.videoDuration = duration); + setVideoDuration = (duration: number) => { + this.videoDuration = duration; + }; @action setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => { @@ -69,15 +71,15 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { public static WorkspaceStopRecording() { const remDoc = RecordingBox.screengrabber?.Document; if (remDoc) { - //if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening + // if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening RecordingBox.screengrabber?.Pause?.(); setTimeout(() => { RecordingBox.screengrabber?.Finish?.(); - remDoc.overlayX = 70; //was 100 + remDoc.overlayX = 70; // was 100 remDoc.overlayY = 590; RecordingBox.screengrabber = undefined; }, 100); - //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this + // could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000); Doc.UserDoc().workspaceRecordingState = mediaState.Paused; Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc); @@ -103,10 +105,10 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { _width: 205, _height: 115, }); - screengrabber.overlayX = 70; //was -400 - screengrabber.overlayY = 590; //was 0 + screengrabber.overlayX = 70; // was -400 + screengrabber.overlayY = 590; // was 0 screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; - Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay + Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; RecordingBox.screengrabber.Record?.(); @@ -137,7 +139,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @undoBatch public static addRecToWorkspace(value: RecordingBox) { - let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); + const ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document); Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.Document); Doc.RemFromMyOverlay(value.Document); @@ -204,51 +206,66 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + // eslint-disable-next-line no-use-before-define static screengrabber: RecordingBox | undefined; } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function stopWorkspaceRecording() { RecordingBox.WorkspaceStopRecording(); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) { RecordingBox.stopWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) { RecordingBox.removeWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getCurrentRecording() { return Doc.UserDoc().currentRecording; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getWorkspaceRecordings() { return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function isWorkspaceRecording() { return Doc.UserDoc().workspaceRecordingState === mediaState.Recording; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function isWorkspaceReplaying() { return Doc.UserDoc().workspaceReplayingState; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function replayWorkspace(value: Doc | string, _readOnly_: boolean) { if (_readOnly_) return DocCast(Doc.UserDoc().currentRecording) ?? 'Record Workspace'; if (typeof value === 'string') RecordingBox.WorkspaceStartRecording(value); else RecordingBox.replayWorkspace(value); + return undefined; }); -ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) { +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc) { RecordingBox.pauseWorkspaceReplaying(value); }); -ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) { +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc) { RecordingBox.resumeWorkspaceReplaying(value); }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) { if (DocCast(value.doc)) { DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY); value.e.preventDefault(); return true; } + return undefined; }); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renderDropdown() { if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) { return true; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index e29e47514..882f6ba9e 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/media-has-caption */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as React from 'react'; // import { Canvas } from '@react-three/fiber'; @@ -163,7 +164,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const subitems = [{ description: 'Screen Capture', event: this.toggleRecording, icon: 'expand-arrows-alt' as any }]; ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' }); }; @@ -171,7 +172,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @computed get content() { return ( <video - className={'videoBox-content'} + className="videoBox-content" key="video" ref={r => { this._videoRef = r; @@ -184,7 +185,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() autoPlay={this._screenCapture} style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }} onCanPlay={this.videoLoad} - controls={true} + controls onClick={e => e.preventDefault()}> <source type="video/mp4" /> Not supported. @@ -221,23 +222,23 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() toggleRecording = async () => { if (!this._screenCapture) { this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); - const aud_chunks: any = []; - this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); - this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file }))); + const audChunks: any = []; + this._audioRec.ondataavailable = (e: any) => audChunks.push(e.data); + this._audioRec.onstop = async () => { + const [{ result }] = await Networking.UploadFilesToServer(audChunks.map((file: any) => ({ file }))); if (!(result instanceof Error)) { this.dataDoc[this._props.fieldKey + '_audio'] = new AudioField(result.accessPaths.agnostic.client); } }; this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); this._videoRec = new MediaRecorder(this._videoRef!.srcObject); - const vid_chunks: any = []; + const vidChunks: any = []; this._videoRec.onstart = () => { if (this.dataDoc[this._props.fieldKey + '_trackScreen']) TrackMovements.Instance.start(); this.dataDoc[this._props.fieldKey + '_recordingStart'] = new DateField(new Date()); }; - this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); - this._videoRec.onstop = async (e: any) => { + this._videoRec.ondataavailable = (e: any) => vidChunks.push(e.data); + this._videoRec.onstop = async () => { const presentation = TrackMovements.Instance.yieldPresentation(); if (presentation?.movements) { const presCopy = { ...presentation }; @@ -245,7 +246,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } TrackMovements.Instance.finish(); - const file = new File(vid_chunks, `${this.Document[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const file = new File(vidChunks, `${this.Document[Id]}.mkv`, { type: vidChunks[0].type, lastModified: Date.now() }); const [{ result }] = await Networking.UploadFilesToServer({ file }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; if (!(result instanceof Error)) { @@ -298,6 +299,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div className="videoBox-viewer"> <div style={{ position: 'relative', height: this.videoPanelHeight() }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} @@ -306,7 +308,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() PanelWidth={this._props.PanelWidth} focus={this._props.focus} isSelected={this._props.isSelected} - isAnnotationOverlay={true} + isAnnotationOverlay select={emptyFunction} isContentActive={returnFalse} NativeDimScaling={returnOne} @@ -325,9 +327,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])} - fieldKey={'text'} + fieldKey="text" PanelHeight={this.formattedPanelHeight} select={emptyFunction} isContentActive={emptyFunction} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 60141b2a6..5b3f37993 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/media-has-caption */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -153,11 +154,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl clearTimeout(this._controlsFadeTimer); this._scrubbing = true; this._controlsFadeTimer = setTimeout( - action(() => (this._scrubbing = false)), + action(() => { + this._scrubbing = false; + }), 500 ); e.stopPropagation(); break; + default: } } }; @@ -204,7 +208,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl else { this._keepCurrentlyPlaying = true; this.pause(); - setTimeout(() => (this._keepCurrentlyPlaying = false)); + setTimeout(() => { + this._keepCurrentlyPlaying = false; + }); } }; @@ -247,7 +253,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl clearTimeout(this._controlsFadeTimer); this._controlsVisible = true; this._controlsFadeTimer = setTimeout( - action(() => (this._controlsVisible = false)), + action(() => { + this._controlsVisible = false; + }), 3000 ); } @@ -281,7 +289,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1); - const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions + const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions if (ctx) { this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); } @@ -298,7 +306,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.addDocument?.(b); DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' }); } else { - //convert to desired file format + // convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, ''); @@ -335,7 +343,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); this._props.addDocument?.(imageSnapshot); - const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); + DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; @@ -346,7 +354,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; const anchor = addAsAnnotation && marquee - ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document + ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); return anchor; @@ -376,12 +384,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (this._stackedTimeline?.makeDocUnfiltered(doc)) { if (this.heightPercent === 100) { // do we want to always open up the timeline when followin a link? kind of clunky visually - //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; + // this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; options.didMove = true; } return this._stackedTimeline.getView(doc, options); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)); + }); }; // extracts video thumbnails and saves them as field of doc @@ -391,7 +401,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const thumbnailPromises: Promise<any>[] = []; const video = document.createElement('video'); - video.onloadedmetadata = () => (video.currentTime = 0); + video.onloadedmetadata = () => { + video.currentTime = 0; + }; video.onseeked = () => { const canvas = document.createElement('canvas'); @@ -405,7 +417,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (newTime < video.duration) { video.currentTime = newTime; } else { - Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails))); + Promise.all(thumbnailPromises).then(thumbnails => { + this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails); + }); } }; @@ -424,11 +438,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._disposers.reactionDisposer?.(); this._disposers.reactionDisposer = reaction( () => NumCast(this.layoutDoc._layout_currentTimecode), - time => !this._playing && (vref.currentTime = time), + time => { + !this._playing && (vref.currentTime = time); + }, { fireImmediately: true } ); - (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails(); + (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoBox.numThumbnails) && this.getVideoThumbnails(); } }; @@ -437,7 +453,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setContentRef = (cref: HTMLDivElement | null) => { this._contentRef = cref; if (cref) { - cref.onfullscreenchange = action(e => { + cref.onfullscreenchange = action(() => { this._fullScreen = document.fullscreenElement === cref; this._controlsVisible = true; this._scrubbing = false; @@ -452,7 +468,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; // context menu - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this._props.fieldKey], VideoField); if (field) { const url = field.url.href; @@ -463,7 +479,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl subitems.push({ description: 'Screen Capture', event: async () => { - runInAction(() => (this._screenCapture = !this._screenCapture)); + runInAction(() => { + this._screenCapture = !this._screenCapture; + }); this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }, icon: 'expand-arrows-alt', @@ -471,7 +489,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' }); subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected', - event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks), + event: () => { + this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks; + }, icon: 'expand-arrows-alt', }); subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' }); @@ -505,7 +525,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl }; // ref for updating time - setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e); + setAudioRef = (e: HTMLAudioElement | null) => { + this._audioPlayer = e; + }; // renders the video and audio @computed get content() { @@ -587,7 +609,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl setupMoveUpEvents( this, e, - action(encodeURIComponent => { + action(() => { this._clicking = false; if (this._props.isContentActive()) { // const local = this.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); @@ -601,7 +623,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl () => { this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; setTimeout( - action(() => (this._clicking = false)), + action(() => { + this._clicking = false; + }), 500 ); }, @@ -636,7 +660,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._layout_currentTimecode); - docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1)); + docs.forEach(doc => { + doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1; + }); return this.addDocument(doc); } @@ -732,11 +758,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl // stretches vertically or horizontally depending on video orientation so video fits full screen fullScreenSize() { if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) { - //prettier-ignore - return ({ height: '100%' }); + return { height: '100%' }; } - //prettier-ignore - return ({ width: '100%' }); + return ({ width: '100%' }); // prettier-ignore } // for zoom slider, sets timeline waveform zoom @@ -778,7 +802,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.select(true); }; - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => { + this._isAnyChildContentActive = isActive; + this._props.whenChildContentsActiveChanged(isActive); + }); timelineScreenToLocal = () => this._props @@ -786,7 +813,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl .scale(this.scaling()) .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight()); - setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time); + setPlayheadTime = (time: number) => { + this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time; + }; timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100; @@ -849,7 +878,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl return ( <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}> <CollectionStackedTimeline - ref={action((r: any) => (this._stackedTimeline = r))} + ref={action((r: any) => { + this._stackedTimeline = r; + })} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} @@ -887,7 +919,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } crop = (region: Doc | undefined, addCrop?: boolean) => { - if (!region) return; + if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); const regionData = region[DocData]; regionData.backgroundColor = 'transparent'; @@ -916,8 +948,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl croppingProto.type = DocumentType.VID; croppingProto.layout = VideoBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto['data_nativeWidth'] = anchw; - croppingProto['data_nativeHeight'] = anchh; + croppingProto.data_nativeWidth = anchw; + croppingProto.data_nativeHeight = anchh; croppingProto.videoCrop = true; croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode; croppingProto.freeform_scale = viewScale; @@ -959,14 +991,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl left: (this._props.PanelWidth() - this.panelWidth()) / 2, }}> <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} renderDepth={this._props.renderDepth + 1} fieldKey={this.annotationKey} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} + isAnnotationOverlay + annotationLayerHostsContent PanelWidth={this._props.PanelWidth} PanelHeight={this._props.PanelHeight} isAnyChildContentActive={returnFalse} @@ -1050,12 +1083,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl </div> )} - <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}> + <div className="videobox-button" title="full screen" onPointerDown={this.onFullDown}> <FontAwesomeIcon icon="expand" /> </div> {!this._fullScreen && width > 300 && ( - <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}> + <div className="videobox-button" title="show timeline" onPointerDown={this.onTimelineHdlDown}> <FontAwesomeIcon icon="eye" /> </div> )} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 446e83dd3..fc2e4bf61 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { htmlToText } from 'html-to-text'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; @@ -15,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, stringHash, Utils } from '../../../Utils'; +import { emptyFunction, stringHash } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -39,8 +41,9 @@ import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; import { LinkInfo } from './LinkDocPreview'; import { PresBox } from './trails'; import './WebBox.scss'; + const { CreateImage } = require('./WebBoxRenderer'); -const _global = (window /* browser */ || global) /* node */ as any; + @observer export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { @@ -142,19 +145,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth(); - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text(); } this.layoutDoc.thumb = undefined; this.Document.thumbLockout = true; // lock to prevent multiple thumb updates. CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) - .then((data_url: any) => { - if (data_url.includes('<!DOCTYPE')) { + .then((dataUrl: any) => { + if (dataUrl.includes('<!DOCTYPE')) { console.log('BAD DATA IN THUMB CREATION'); return; } - Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => setTimeout( action(() => { this.Document.thumbLockout = false; @@ -167,7 +170,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ) ); }) - .catch(function (error: any) { + .catch((error: any) => { console.error('oops, something went wrong!', error); }); }; @@ -188,7 +191,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }); this._disposers.urlchange = reaction( () => WebCast(this.dataDoc.data), - url => this.submitURL(false, false) + () => this.submitURL(false, false) ); this._disposers.titling = reaction( () => StrCast(this.Document.title), @@ -200,8 +203,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._disposers.layout_autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, - layout_autoHeight => { - if (layout_autoHeight) { + layoutAutoHeight => { + if (layoutAutoHeight) { this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']); this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1)); } @@ -221,7 +224,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { WebRequest.get(ClientUtils.CorsProxy(this.webField.href)) // - .then(result => result && (this.dataDoc.text = htmlToText(result.content))); + .then(result => { + result && (this.dataDoc.text = htmlToText(result.content)); + }); } this._disposers.scrollReaction = reaction( @@ -297,21 +302,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const focusTime = options.zoomTime ?? 500; this.goTo(scrollTo, focusTime, options.easeFunc); return focusTime; - } else { - this._initialScroll = scrollTo; } + this._initialScroll = scrollTo; } } + return undefined; }; @action - getView = (doc: Doc, options: FocusViewOptions) => { - if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.())); + getView = (doc: Doc /* , options: FocusViewOptions */) => { + if (Doc.AreProtosEqual(doc, this.Document)) + return new Promise<Opt<DocumentView>>(res => { + res(this.DocumentView?.()); + }); if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify(); const webUrl = WebCast(doc.config_data)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)); + }); }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { @@ -322,14 +332,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return this._props.addDocTab(doc, where); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - let ele: Opt<HTMLDivElement> = undefined; + let ele: Opt<HTMLDivElement>; try { const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { ele = document.createElement('div'); ele.append(contents); } - } catch (e) {} + } catch (e) { + /* empty */ + } const visibleAnchor = this._getAnchor(this._savedAnnotations, true); const anchor = visibleAnchor ?? @@ -338,7 +350,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem y: NumCast(this.layoutDoc._layout_scrollTop), annotationOn: this.Document, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.Document); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: !!pinProps?.pinData, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML ?? this._selectionText; addAsAnnotation && this.addDocumentWrapper(anchor); @@ -395,7 +407,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e?.stopPropagation(); setTimeout(() => { // if menu comes up right away, the down event can still be active causing a menu item to be selected - this.specificContextMenu(undefined as any); + this.specificContextMenu(); this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]); }); } @@ -470,6 +482,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const sheets = document.head.appendChild(style); return (sheets as any).sheet; } + return undefined; } addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = @@ -484,7 +497,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem _iframetimeout: any = undefined; @observable _warning = 0; @action - iframeLoaded = (e: any) => { + iframeLoaded = () => { const iframe = this._iframe; if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); @@ -640,12 +653,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => (this._webUrl = this._url))); + setTimeout( + action(() => { + this._webUrl = this._url; + }) + ); } else { this._webUrl = this._url; } return true; } + return undefined; }); return false; }; @@ -663,12 +681,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout(action(() => (this._webUrl = this._url))); + setTimeout( + action(() => { + this._webUrl = this._url; + }) + ); } else { this._webUrl = this._url; } return true; } + return undefined; }); return false; }; @@ -700,7 +723,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const html = dataTransfer.getData('text/html'); const uri = dataTransfer.getData('text/uri-list'); const url = uri || html || this._url || ''; - const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; + const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^/]*/)?.[0] || '') : url; this.setData(newurl); e.stopPropagation(); }; @@ -723,19 +746,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.stopPropagation(); }; - specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { + specificContextMenu = (): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; if (!cm.findByDescription('Options...')) { !Doc.noviceMode && - funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' }); + funcs.push({ + description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', + event: () => { + this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']; + }, + icon: 'snowflake', + }); funcs.push({ description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts', event: () => { this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts']; if (this._iframe) { - runInAction(() => (this._hackHide = true)); - setTimeout(action(() => (this._hackHide = false))); + runInAction(() => { + this._hackHide = true; + }); + setTimeout( + action(() => { + this._hackHide = false; + }) + ); } }, icon: 'snowflake', @@ -773,7 +808,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - action(e => { + action(() => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); return true; }), @@ -797,7 +832,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @observable lighttext = false; @computed get urlContent() { - if (this.ScreenToLocalBoxXf().Scale > 25) return <div></div>; + if (this.ScreenToLocalBoxXf().Scale > 25) return <div />; setTimeout( action(() => { if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { @@ -826,12 +861,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.CorsProxy(this._webUrl) : this._webUrl; const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing'); - //if (!scripts) console.log('No scripts for: ' + url); + // if (!scripts) console.log('No scripts for: ' + url); return ( <iframe + title="web iframe" key={this._warning} className="webBox-iframe" - ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }} src={url} onLoad={this.iframeLoaded} @@ -842,11 +880,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /> ); } - return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />; + return ( + <iframe + title="web frame" + className="webBox-iframe" + ref={action((r: HTMLIFrameElement | null) => { + this._iframe = r; + })} + src="https://crossorigin.me/https://cs.brown.edu" + /> + ); } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - this._url && (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url))); + this._url && + (doc instanceof Doc ? [doc] : doc).forEach(doc => { + doc.config_data = new WebField(this._url); + }); return this.addDocument(doc, annotationKey); }; @@ -900,14 +950,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); + let nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); if (!nativeWidth) { const defaultNativeWidth = NumCast(this.Document.nativeWidth, this.dataDoc[this.fieldKey] instanceof WebField ? 850 : NumCast(this.Document._width)); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); @@ -946,7 +996,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; _innerCollectionView: CollectionFreeFormView | undefined; zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; - setInnerContent = (component: ViewBoxInterface) => (this._innerCollectionView = component as CollectionFreeFormView); + setInnerContent = (component: ViewBoxInterface) => { + this._innerCollectionView = component as CollectionFreeFormView; + }; @computed get content() { const interactive = this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; @@ -977,24 +1029,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( + // eslint-disable-next-line react/jsx-props-no-spreading <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> ))} </div> ); } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } renderAnnotations = (childFilters: () => string[]) => ( <CollectionFreeFormView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={this.setInnerContent} NativeWidth={returnZero} NativeHeight={returnZero} originTopLeft={false} - isAnnotationOverlayScrollable={true} + isAnnotationOverlayScrollable renderDepth={this._props.renderDepth + 1} - isAnnotationOverlay={true} + isAnnotationOverlay fieldKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} PanelWidth={this.panelWidth} @@ -1037,7 +1091,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }} // when active, block wheel events from propagating since they're handled by the iframe onWheel={this.onZoomWheel} - onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} + onScroll={() => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight > this._props.PanelHeight() && this._scrollHeight) || '100%', pointerEvents }}> {this.content} @@ -1053,7 +1107,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return ( <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> <div className="webBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> - <button className="webBox-overlayButton" title={'search'} /> + <button type="button" className="webBox-overlayButton" title="search" /> <input className="webBox-searchBar" placeholder="Search" @@ -1064,13 +1118,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.stopPropagation(); }} /> - <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <button type="button" className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> </div> <button + type="button" className="webBox-overlayButton" - title={'search'} + title="search" onClick={action(() => { this._searching = !this._searching; this.search('', false, true); @@ -1083,8 +1138,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </div> ); } - searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.currentTarget.value; + }; + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => { + this._setPreviewCursor = func; + }; panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); @@ -1157,6 +1216,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} @@ -1177,6 +1237,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ); } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function urlHash(url: string) { return stringHash(url); }); diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index eaa8fffaa..dc388b22a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; @@ -24,90 +27,69 @@ import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -export class DashFieldView { - dom: HTMLDivElement; // container for label and value - root: any; - node: any; - tbox: FormattedTextBox; - getpos: any; - @observable _nodeSelected = false; - NodeSelected = () => this._nodeSelected; +@observer +export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define + static Instance: DashFieldViewMenu; + static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + static toggleFieldHide: () => void = emptyFunction; + static toggleValueHide: () => void = emptyFunction; + constructor(props: any) { + super(props); + DashFieldViewMenu.Instance = this; + } - unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { - makeObservable(this); - const self = this; - this.node = node; - this.tbox = tbox; - this.getpos = getPos; - this.dom = document.createElement('div'); - this.dom.style.width = node.attrs.width; - this.dom.style.height = node.attrs.height; - this.dom.style.position = 'relative'; - this.dom.style.display = 'inline-block'; - const tBox = this.tbox; - this.dom.onkeypress = function (e: KeyboardEvent) { - e.stopPropagation(); - }; - this.dom.onkeydown = function (e: KeyboardEvent) { - e.stopPropagation(); - if (e.key === 'Tab') { - e.preventDefault(); - const editor = tbox.EditorView; - if (editor) { - const state = editor.state; - for (var i = self.getpos() + 1; i < state.doc.content.size; i++) { - if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); - return; - } - } - // tBox.setFocus(state.selection.to); - } - } - }; - this.dom.onkeyup = function (e: any) { - e.stopPropagation(); - }; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; + showFields = (e: React.MouseEvent) => { + DashFieldViewMenu.createFieldView(e); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleFieldHide = () => { + DashFieldViewMenu.toggleFieldHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleValueHide = () => { + DashFieldViewMenu.toggleValueHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; - this.root = ReactDOM.createRoot(this.dom); - this.root.render( - <DashFieldViewInternal - node={node} - unclickable={this.unclickable} - getPos={getPos} - fieldKey={node.attrs.fieldKey} - docId={node.attrs.docId} - width={node.attrs.width} - height={node.attrs.height} - hideKey={node.attrs.hideKey} - hideValue={node.attrs.hideValue} - editable={node.attrs.editable} - nodeSelected={this.NodeSelected} - tbox={tbox} - /> + @observable _fieldKey = ''; + + @action + public show = (x: number, y: number, fieldKey: string) => { + this._fieldKey = fieldKey; + this.jumpTo(x, y, true); + const hideMenu = () => { + this.fadeOut(true); + document.removeEventListener('pointerdown', hideMenu, true); + }; + document.addEventListener('pointerdown', hideMenu, true); + }; + render() { + return this.getElement( + <> + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="sm" /> + </button> + </Tooltip> + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> + <FontAwesomeIcon icon="bullseye" size="sm" /> + </button> + </Tooltip> + )} + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}> + <button type="button" className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> + <FontAwesomeIcon icon="hashtag" size="sm" /> + </button> + </Tooltip> + )} + </> ); } - destroy() { - setTimeout(() => { - try { - this.root.unmount(); - } catch {} - }); - } - deselectNode() { - runInAction(() => (this._nodeSelected = false)); - this.dom.classList.remove('ProseMirror-selectednode'); - } - selectNode() { - setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100); - this.dom.classList.add('ProseMirror-selectednode'); - } } - interface IDashFieldViewInternal { fieldKey: string; docId: string; @@ -137,7 +119,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi makeObservable(this); this._fieldKey = this._props.fieldKey; this._textBoxDoc = this._props.tbox.Document; - const setDoc = action((doc: Doc) => (this._dashDoc = doc)); + const setDoc = action((doc: Doc) => { + this._dashDoc = doc; + }); if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); @@ -172,7 +156,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { return !this._dashDoc ? null : ( - <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> + <div + onClick={action(() => { + this._expanded = !this._props.editable ? !this._expanded : true; + })} + style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} @@ -187,20 +175,20 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} - allowCRs={true} + allowCRs oneLine={!this._expanded && !this._props.nodeSelected()} finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} - autoFocus={true} + autoFocus rootSelected={this._props.tbox._props.rootSelected} /> </div> ); } - createPivotForField = (e: React.MouseEvent) => { - let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); + createPivotForField = () => { + const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; @@ -219,7 +207,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi toggleFieldHide = undoable( action(() => { const editor = this._props.tbox.EditorView!; - editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false })); + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey })); }), 'hideKey' ); @@ -227,7 +215,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi toggleValueHide = undoable( action(() => { const editor = this._props.tbox.EditorView!; - editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false })); + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue })); }), 'hideValue' ); @@ -243,11 +231,11 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi // clicking on the label creates a pivot view collection of all documents // in the same collection. The pivot field is the fieldKey of this label onPointerDownLabelSpan = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, moveEv => { DashFieldViewMenu.createFieldView = this.createPivotForField; DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide; DashFieldViewMenu.toggleValueHide = this.toggleValueHide; - DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); + DashFieldViewMenu.Instance.show(moveEv.clientX, moveEv.clientY + 16, this._fieldKey); const editor = this._props.tbox.EditorView!; setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100); }); @@ -277,7 +265,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }}> {this._hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey} + {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : (this._dashDoc?.title ?? '') + ':') + this._fieldKey} </span> )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} @@ -293,65 +281,93 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi ); } } -@observer -export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: DashFieldViewMenu; - static createFieldView: (e: React.MouseEvent) => void = emptyFunction; - static toggleFieldHide: () => void = emptyFunction; - static toggleValueHide: () => void = emptyFunction; - constructor(props: any) { - super(props); - DashFieldViewMenu.Instance = this; - } - - showFields = (e: React.MouseEvent) => { - DashFieldViewMenu.createFieldView(e); - DashFieldViewMenu.Instance.fadeOut(true); - }; - toggleFieldHide = (e: React.MouseEvent) => { - DashFieldViewMenu.toggleFieldHide(); - DashFieldViewMenu.Instance.fadeOut(true); - }; - toggleValueHide = (e: React.MouseEvent) => { - DashFieldViewMenu.toggleValueHide(); - DashFieldViewMenu.Instance.fadeOut(true); - }; - - @observable _fieldKey = ''; +export class DashFieldView { + dom: HTMLDivElement; // container for label and value + root: any; + node: any; + tbox: FormattedTextBox; + getpos: any; + @observable _nodeSelected = false; + NodeSelected = () => this._nodeSelected; - @action - public show = (x: number, y: number, fieldKey: string) => { - this._fieldKey = fieldKey; - this.jumpTo(x, y, true); - const hideMenu = () => { - this.fadeOut(true); - document.removeEventListener('pointerdown', hideMenu, true); + unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + makeObservable(this); + const self = this; + this.node = node; + this.tbox = tbox; + this.getpos = getPos; + this.dom = document.createElement('div'); + this.dom.style.width = node.attrs.width; + this.dom.style.height = node.attrs.height; + this.dom.style.position = 'relative'; + this.dom.style.display = 'inline-block'; + this.dom.onkeypress = function (e: KeyboardEvent) { + e.stopPropagation(); }; - document.addEventListener('pointerdown', hideMenu, true); - }; - render() { - return this.getElement( - <> - <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.showFields}> - <FontAwesomeIcon icon="eye" size="sm" /> - </button> - </Tooltip> - {this._fieldKey.startsWith('#') ? null : ( - <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> - <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> - <FontAwesomeIcon icon="bullseye" size="sm" /> - </button> - </Tooltip> - )} - {this._fieldKey.startsWith('#') ? null : ( - <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}> - <button className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> - <FontAwesomeIcon icon="hashtag" size="sm" /> - </button> - </Tooltip> - )} - </> + this.dom.onkeydown = function (e: KeyboardEvent) { + e.stopPropagation(); + if (e.key === 'Tab') { + e.preventDefault(); + const editor = tbox.EditorView; + if (editor) { + const { state } = editor; + for (let i = self.getpos() + 1; i < state.doc.content.size; i++) { + if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { + editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); + return; + } + } + } + } + }; + this.dom.onkeyup = function (e: any) { + e.stopPropagation(); + }; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashFieldViewInternal + node={node} + unclickable={this.unclickable} + getPos={getPos} + fieldKey={node.attrs.fieldKey} + docId={node.attrs.docId} + width={node.attrs.width} + height={node.attrs.height} + hideKey={node.attrs.hideKey} + hideValue={node.attrs.hideValue} + editable={node.attrs.editable} + nodeSelected={this.NodeSelected} + tbox={tbox} + /> ); } + destroy() { + setTimeout(() => { + try { + this.root.unmount(); + } catch { + /* empty */ + } + }); + } + deselectNode() { + runInAction(() => { + this._nodeSelected = false; + }); + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + setTimeout( + action(() => { + this._nodeSelected = true; + }), + 100 + ); + this.dom.classList.add('ProseMirror-selectednode'); + } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 5b2b558fc..3a1a72910 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -350,7 +350,8 @@ footnote::before { span { font-family: inherit; background-color: inherit; - display: inline; // needs to be inline for search highlighting to appear // contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span + display: inline; // needs to be inline for search highlighting to appear + // display: contents; // BUT needs to be 'contents' to avoid Chrome bug where extra space is added above and <ol> lists when inside a prosemirror span } blockquote { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a82f025f9..99a2f4ab9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1531,7 +1531,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { fontFamily: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ...[schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })], diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index c3a5a2c86..073ed91c3 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -23,7 +23,7 @@ export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle tx2.doc.descendants((node: any, offset: any /* , index: any */) => { if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { const { path } = tx2.doc.resolve(offset) as any; - let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty('type') && c.type === schema.nodes.ordered_list ? 1 : 0), 0); + let depth = Array.from(path).reduce((p: number, c: any) => p + (c.type === schema.nodes.ordered_list ? 1 : 0), 0); if (node.type === schema.nodes.ordered_list) { if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle; depth++; @@ -34,18 +34,19 @@ export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle return tx2; }; -export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { +export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMap { const keys: { [key: string]: any } = {}; function bind(key: string, cmd: any) { - if (mapKeys) { - const mapped = mapKeys[key]; - if (mapped === false) return; - if (mapped) key = mapped; - } keys[key] = cmd; } + function onKey(): boolean | undefined { + // bcz: this is pretty hacky -- prosemirror doesn't send us the keyboard event, but the 'event' variable is in scope.. so we access it anyway + // eslint-disable-next-line no-restricted-globals + return props.onKey?.(event, props); + } + const canEdit = (state: any) => { switch (GetEffectiveAcl(props.TemplateDataDocument)) { case AclAugment: @@ -84,12 +85,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey // Commands for lists bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); - bind('Ctrl-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Alt-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Meta-Tab', () => (props.onKey?.(event, props) ? true : true)); - bind('Meta-Enter', () => (props.onKey?.(event, props) ? true : true)); + bind('Ctrl-Tab', () => onKey() || true); + bind('Alt-Tab', () => onKey() || true); + bind('Meta-Tab', () => onKey() || true); + bind('Meta-Enter', () => onKey() || true); bind('Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); @@ -119,10 +120,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey console.log('bullet promote fail'); } } + return undefined; }); bind('Shift-Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -136,10 +138,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey ) { console.log('bullet demote fail'); } + return undefined; }); // Command to create a new Tab with a PDF of all the command shortcuts - bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Mod-/', () => { const newDoc = Docs.Create.PdfDocument(ClientUtils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); props.addDocTab(newDoc, OpenWhere.addRight); }); @@ -171,13 +174,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey SelectionManager.DeselectAll(); }); - bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true)); - bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true)); + bind('Alt-Enter', () => onKey() || true); + bind('Ctrl-Enter', () => onKey() || true); bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => { dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); return true; }); - bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Cmd-?', () => { RTFMarkup.Instance.open(); return true; }); @@ -200,7 +203,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -215,7 +218,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -230,7 +233,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; if (node) { - tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]); + tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]); } } dispatch(tr); @@ -262,7 +265,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; if ( @@ -296,7 +299,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey // command to break line const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { - if (props.onKey?.(event, props)) return true; + if (onKey()) return true; if (!canEdit(state)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); @@ -361,8 +364,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Enter', enter); // Command to create a blank space - bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true; + bind('Space', () => { + if (props.TemplateDataDocument && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(props.TemplateDataDocument))) return true; return false; }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index ec9c1a15d..6108383c2 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -23,10 +23,12 @@ import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; + const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); static get Instance() { return RichTextMenu._instance?.menu; @@ -116,7 +118,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction( () => SelectionManager.Views.slice(), - views => this.updateMenu(undefined, undefined, undefined, undefined) + () => this.updateMenu(undefined, undefined, undefined, undefined) ); } componentWillUnmount() { @@ -139,10 +141,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); - const activeFamilies = active.activeFamilies; - const activeSizes = active.activeSizes; - const activeColors = active.activeColors; - const activeHighlights = active.activeHighlights; + const { activeFamilies } = active; + const { activeSizes } = active; + const { activeColors } = active; + const { activeHighlights } = active; const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); @@ -174,8 +176,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { dispatch(updateBullets(markup, state.schema)); } else { const state = this.view?.state; - const tr = this.view?.state.tr; - if (tr && state) { + if (state) { + const { tr } = state; if (dontToggle) { tr.addMark(state.selection.from, state.selection.to, mark); dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise @@ -190,7 +192,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveAlignment() { if (this.view && this.TextView?._props.rootSelected?.()) { - const path = (this.view.state.selection.$from as any).path; + const { path } = this.view.state.selection.$from as any; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { return path[i].attrs.align || 'left'; @@ -222,24 +224,25 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const activeColors = new Set<string>(); const activeHighlights = new Set<string>(); if (this.view && this.TextView?._props.rootSelected?.()) { - const state = this.view.state; + const { state } = this.view; const pos = this.view.state.selection.$from; - var marks: Mark[] = [...(state.storedMarks ?? [])]; + let marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { + /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family); - m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color); + m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.fontFamily); + m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.fontColor); m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); - m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); + m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHigh)); }); } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); @@ -254,26 +257,27 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return found; } - //finds all active marks on selection in given group + // finds all active marks on selection in given group getActiveMarksOnSelection() { if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - const state = this.view.state; - var marks: Mark[] = [...(state.storedMarks ?? [])]; + const { state } = this.view; + let marks: Mark[] = [...(state.storedMarks ?? [])]; const pos = this.view.state.selection.$from; if (state.storedMarks !== null) { + /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; - return markGroup.filter(mark_type => { - const mark = state.schema.mark(mark_type); + return markGroup.filter(markType => { + const mark = state.schema.mark(markType); return mark.isInSet(marks); }); } @@ -291,7 +295,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._superscriptActive = false; activeMarks.forEach(mark => { - // prettier-ignore switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; case 'strong': this._boldActive = true; break; @@ -300,7 +303,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { case 'strikethrough': this._strikethroughActive = true; break; case 'subscript': this._subscriptActive = true; break; case 'superscript': this._superscriptActive = true; break; - } + default: + } // prettier-ignore }); } @@ -353,53 +357,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - setFontSize = (fontSize: string) => { + setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.view) { - if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { - this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize; + if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === value))) { + this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value; this.view.focus(); } else { - const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); + const attrs: { [key: string]: string } = {}; + attrs[fontField] = value; + const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontSize'] = fontSize)); - } else Doc.UserDoc().fontSize = fontSize; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); - }; - - setFontFamily = (family: string) => { - if (this.view) { - const fmark = this.view.state.schema.marks.pFontFamily.create({ family }); - this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); - this.view.focus(); - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontFamily'] = family)); - } else Doc.UserDoc().fontFamily = family; + } else Doc.UserDoc()[fontField] = value; this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; - setHighlight(color: string) { - if (this.view) { - const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color }); - this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true); - this.view.focus(); - } else Doc.UserDoc()._fontHighlight = color; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); - } - - setColor(color: string) { - if (this.view) { - const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color }); - this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); - this.view.focus(); - } else if (SelectionManager.Views.length) { - SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontColor'] = color)); - } else Doc.UserDoc().fontColor = color; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); - } - // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { @@ -407,7 +380,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const newMapStyle = active === mapStyle ? '' : mapStyle; if (!this.view || newMapStyle === '') return; - let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; + const inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); if (inList) { const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); @@ -428,7 +401,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { insertSummarizer(state: EditorState, dispatch: any) { if (state.selection.empty) return false; const mark = state.schema.marks.summarize.create(); - const tr = state.tr; + const { tr } = state; tr.addMark(state.selection.from, state.selection.to, mark); const content = tr.selection.content(); const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); @@ -436,13 +409,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } - vcenterToggle = (view: EditorView, dispatch: any) => { + vcenterToggle = () => { this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); }; align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { if (this.TextView?._props.rootSelected?.()) { - var tr = view.state.tr; - view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { + let { tr } = view.state; + view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks); return false; @@ -455,56 +428,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - insetParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + paragraphSetup(state: EditorState, dispatch: any, field: 'inset' | 'indent', value?: 0 | 10 | -10) { + let { tr } = state; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); - return false; - } - return true; - }); - dispatch?.(tr); - return true; - } - outsetParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); - return false; - } - return true; - }); - dispatch?.(tr); - return true; - } - - indentParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - const heading = false; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; - const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); - return false; - } - return true; - }); - !heading && dispatch?.(tr); - return true; - } - - hangingIndentParagraph(state: EditorState, dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { - const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; - const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; - tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + const newValue = !value ? + (node.attrs[field] ? 0 : node.attrs[field] + 10) : + Math.max(0, value); // prettier-ignore + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, ...(field === 'inset' ? { inset: newValue } : { indent: newValue }) }, node.marks); return false; } return true; @@ -514,7 +445,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } insertBlockquote(state: EditorState, dispatch: any) { - const path = (state.selection.$from as any).path; + const { path } = state.selection.$from as any; if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { lift(state, dispatch); } else { @@ -547,13 +478,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } @action - fillBrush(state: EditorState, dispatch: any) { + fillBrush() { if (!this.view) return; if (!Array.from(this.brushMarks.keys()).length) { - const selected_marks = this.getMarksInSelection(this.view.state); - if (selected_marks.size >= 0) { - this.brushMarks = selected_marks; + const selectedMarks = this.getMarksInSelection(this.view.state); + if (selectedMarks.size >= 0) { + this.brushMarks = selectedMarks; } } else { const { from, to, $from } = this.view.state.selection; @@ -597,9 +528,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const button = ( <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button"> - <FontAwesomeIcon icon="link" size="lg" /> - </button> + { + // eslint-disable-next-line jsx-a11y/control-has-associated-label + <button type="button" className="antimodeMenu-button color-preview-button"> + <FontAwesomeIcon icon="link" size="lg" /> + </button> + } </Tooltip> ); @@ -607,21 +541,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { <div className="dropdown link-menu"> <p>Linked to:</p> <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} /> - <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, 'add:right')}> + <button type="button" className="make-button" onPointerDown={() => this.makeLinkToURL(link)}> Apply hyperlink </button> <div className="divider" /> - <button className="remove-button" onPointerDown={e => this.deleteLink()}> + <button type="button" className="remove-button" onPointerDown={() => this.deleteLink()}> Remove link </button> </div> ); - return <ButtonDropdown view={this.view} key={'link button'} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />; + // eslint-disable-next-line no-use-before-define + return <ButtonDropdown view={this.view} key="link button" button={button} dropdownContent={dropdownContent} openDropdownOnButton link />; } async getTextLinkTargetTitle() { - if (!this.view) return; + if (!this.view) return undefined; const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === 'link'); @@ -633,15 +568,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (linkclicked) { const linkDoc = await DocServer.GetRefField(linkclicked); if (linkDoc instanceof Doc) { - const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc); - const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc); + const linkAnchor1 = await Cast(linkDoc.link_anchor_1, Doc); + const linkAnchor2 = await Cast(linkDoc.link_anchor_2, Doc); const currentDoc = SelectionManager.Docs.lastElement(); - if (currentDoc && link_anchor_1 && link_anchor_2) { - if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) { - return StrCast(link_anchor_2.title); + if (currentDoc && linkAnchor1 && linkAnchor2) { + if (Doc.AreProtosEqual(currentDoc, linkAnchor1)) { + return StrCast(linkAnchor2.title); } - if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) { - return StrCast(link_anchor_1.title); + if (Doc.AreProtosEqual(currentDoc, linkAnchor2)) { + return StrCast(linkAnchor1.title); } } } @@ -653,11 +588,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return link.attrs.title; } } + return undefined; } // TODO: should check for valid URL @undoBatch - makeLinkToURL = (target: string, lcoation: string) => { + makeLinkToURL = (target: string) => { ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); }; @@ -736,7 +672,11 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps render() { return ( - <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}> + <div + className="button-dropdown-wrapper" + ref={node => { + this.ref = node; + }}> {!this._props.pdf ? ( <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> {this._props.button} @@ -747,9 +687,12 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps ) : ( <> {this._props.button} - <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> - <FontAwesomeIcon icon="caret-down" size="sm" /> - </button> + { + // eslint-disable-next-line jsx-a11y/control-has-associated-label + <button type="button" className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> + } </> )} {this.showDropdown ? this._props.dropdownContent : null} @@ -762,10 +705,11 @@ interface RichTextMenuPluginProps { editorProps: any; } export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { - render() { - return null; - } + // eslint-disable-next-line react/no-unused-class-component-methods update(view: EditorView, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc); } + render() { + return null; + } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 5b5617484..88adab66e 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -263,7 +263,7 @@ export class RichTextRules { }; if (isValidColor(color)) { - return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color })); + return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ fontColor: color })); } return null; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index d1c7b72a5..6e1f325cf 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -127,29 +127,29 @@ export const marks: { [index: string]: MarkSpec } = { /* FONTS */ pFontFamily: { - attrs: { family: { default: '' } }, + attrs: { fontFamily: { default: '' } }, parseDOM: [ { tag: 'span', getAttrs(dom: any) { const cstyle = getComputedStyle(dom); if (cstyle.font) { - if (cstyle.font.indexOf('Times New Roman') !== -1) return { family: 'Times New Roman' }; - if (cstyle.font.indexOf('Arial') !== -1) return { family: 'Arial' }; - if (cstyle.font.indexOf('Georgia') !== -1) return { family: 'Georgia' }; - if (cstyle.font.indexOf('Comic Sans') !== -1) return { family: 'Comic Sans MS' }; - if (cstyle.font.indexOf('Tahoma') !== -1) return { family: 'Tahoma' }; - if (cstyle.font.indexOf('Crimson') !== -1) return { family: 'Crimson Text' }; + if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' }; + if (cstyle.font.indexOf('Arial') !== -1) return { fontFamily: 'Arial' }; + if (cstyle.font.indexOf('Georgia') !== -1) return { fontFamily: 'Georgia' }; + if (cstyle.font.indexOf('Comic Sans') !== -1) return { fontFamily: 'Comic Sans MS' }; + if (cstyle.font.indexOf('Tahoma') !== -1) return { fontFamily: 'Tahoma' }; + if (cstyle.font.indexOf('Crimson') !== -1) return { fontFamily: 'Crimson Text' }; } - return { family: '' }; + return { fontFamily: '' }; }, }, ], - toDOM: node => (node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0]), + toDOM: node => (node.attrs.fontFamily ? ['span', { style: `font-family: "${node.attrs.fontFamily}";` }] : ['span', 0]), }, // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text. pFontColor: { - attrs: { color: { default: '' } }, + attrs: { fontColor: { default: '' } }, inclusive: true, parseDOM: [ { @@ -159,24 +159,24 @@ export const marks: { [index: string]: MarkSpec } = { }, }, ], - toDOM: node => (node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]), + toDOM: node => (node.attrs.fontColor ? ['span', { style: 'color:' + node.attrs.fontColor }] : ['span', 0]), }, - marker: { + pFontHighlight: { attrs: { - highlight: { default: 'transparent' }, + fontHighlight: { default: 'transparent' }, }, inclusive: true, parseDOM: [ { tag: 'span', getAttrs(dom: any) { - return { highlight: dom.getAttribute('backgroundColor') }; + return { fontHighlight: dom.getAttribute('background-color') }; }, }, ], toDOM(node: any) { - return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }]; + return node.attrs.fontHighlight ? ['span', { style: 'background-color:' + node.attrs.fontHighlight }] : ['span', { style: 'background-color: transparent' }]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 70b6604ab..184487b7d 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -4,14 +4,14 @@ import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './Para import { DocServer } from '../../../DocServer'; import { Doc, Field, FieldType } from '../../../../fields/Doc'; -const blockquoteDOM: DOMOutputSpec = ['blockquote', 0], - hrDOM: DOMOutputSpec = ['hr'], - preDOM: DOMOutputSpec = ['pre', ['code', 0]], - brDOM: DOMOutputSpec = ['br'], - ulDOM: DOMOutputSpec = ['ul', 0]; +const blockquoteDOM: DOMOutputSpec = ['blockquote', 0]; +const hrDOM: DOMOutputSpec = ['hr']; +const preDOM: DOMOutputSpec = ['pre', ['code', 0]]; +const brDOM: DOMOutputSpec = ['br']; +// const ulDOM: DOMOutputSpec = ['ul', 0]; -function formatAudioTime(time: number) { - time = Math.round(time); +function formatAudioTime(timeIn: number) { + const time = Math.round(timeIn); const hours = Math.floor(time / 60 / 60); const minutes = Math.floor(time / 60) - hours * 60; const seconds = time % 60; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 518bf66cd..a3b1a419b 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; @@ -13,7 +15,6 @@ import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { AudioField } from '../../../../fields/URLField'; import { emptyFunction, emptyPath, stringHash } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; @@ -30,7 +31,7 @@ import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; import { CollectionView } from '../../collections/CollectionView'; import { TreeView } from '../../collections/TreeView'; -import { pinDataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent'; +import { pinDataTypes as dataTypes, PinProps, ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; @@ -38,6 +39,7 @@ import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; +import { SettingsManager } from '../../../util/SettingsManager'; @observer export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -61,6 +63,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things _presTimer: NodeJS.Timeout | undefined; + // eslint-disable-next-line no-use-before-define @observable public static Instance: PresBox; @observable _isChildActive = false; @@ -118,6 +121,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get selectedDocumentView() { if (SelectionManager.Views.length) return SelectionManager.Views[0]; if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.Document); + return undefined; } @computed get isPres() { return this.selectedDoc === this.Document; @@ -172,7 +176,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => this.layoutDoc.presentation_status === PresStatus.Edit, editing => editing && this.childDocs.filter(doc => doc.presentation_indexed !== undefined).forEach(doc => { - this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); + this.progressivizedItems(doc)?.forEach(indexedDoc => { indexedDoc.opacity = undefined; }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); }) // prettier-ignore ); @@ -202,13 +206,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time + // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (slideNum?: number) => { const nextSlideInd = slideNum ?? this.itemIndex + 1; let curSlideInd = nextSlideInd; - //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); + // CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); this.clearSelectedArray(); const doGroupWithUp = (nextSelected: number, force = false) => @@ -220,7 +224,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { const waitTime = NumCast(this.activeItem.presentation_duration); - await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime))); + await new Promise<void>(res => { + setTimeout(res, Math.max(0, waitTime)); + }); doGroupWithUp(nextSelected + 1)(); }); } else { @@ -239,8 +245,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presentation_indexed !== undefined && targetList) { const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); - return listItems.filter(doc => !doc.layout_unrendered); + return listItems.filter(ldoc => !ldoc.layout_unrendered); } + return undefined; }; // go to documents chain @@ -259,7 +266,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem); targetRenderedDoc._dataTransition = 'all 1s'; targetRenderedDoc.opacity = 1; - setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000); + setTimeout(() => { + targetRenderedDoc._dataTransition = 'inherit'; + }, 1000); const listItems = this.progressivizedItems(this.activeItem); if (listItems && presIndexed < listItems.length) { if (!first) { @@ -280,6 +289,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return true; } } + return undefined; }; if (progressiveReveal(false)) return true; if (this.childDocs[this.itemIndex + 1] !== undefined) { @@ -289,7 +299,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // before moving onto next slide, run the subroutines :) const currentDoc = this.childDocs[this.itemIndex]; - //could i do this.childDocs[this.itemIndex] for first arg? + // could i do this.childDocs[this.itemIndex] for first arg? this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]); this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); @@ -309,7 +319,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Called when the user activates 'back' - to move to the previous part of the pres. trail @action back = () => { - const activeItem: Doc = this.activeItem; + const { activeItem } = this; let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem.presentation_movement; @@ -328,8 +338,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return this.itemIndex; }; - //The function that is called when a document is clicked or reached through next or back. - //it'll also execute the necessary actions if presentation is playing. + // The function that is called when a document is clicked or reached through next or back. + // it'll also execute the necessary actions if presentation is playing. @undoBatch public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => { Doc.UnBrushAllDocs(); @@ -346,13 +356,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); - this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array + this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array this.turnOffEdit(); - this.navigateToActiveItem(finished); //Handles movement to element only when presentationTrail is list - this.doHideBeforeAfter(); //Handles hide after/before + this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list + this.doHideBeforeAfter(); // Handles hide after/before } }); - static pinDataTypes(target?: Doc): pinDataTypes { + static pinDataTypes(target?: Doc): dataTypes { const targetType = target?.type as any; const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; @@ -363,19 +373,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const datarange = [DocumentType.FUNCPLOT].includes(targetType); const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; - const type_collection = targetType === DocumentType.COL; + const typeCollection = targetType === DocumentType.COL; const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action - playAnnotation = (anno: AudioField) => {}; + playAnnotation = (/* anno: AudioField */) => { + /* empty */ + }; @action - static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { + // eslint-disable-next-line default-param-last + static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) { const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); - if (!bestTarget) return; + if (!bestTarget) return undefined; let changed = false; if (pinDocLayout) { if ( @@ -392,20 +405,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]); - bestTarget[TransitionTimer] = setTimeout(() => (bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined), transTime + 10); + bestTarget[TransitionTimer] = setTimeout(() => { + bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined; + }, transTime + 10); changed = true; } } const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { - const transTime = NumCast(activeItem.presentation_transition, 500); + const frameTime = NumCast(activeItem.presentation_transition, 500); const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc); const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; if (context) { const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.CollectionFreeFormView; if (ffview?.childDocs) { - PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime); + PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, frameTime); ffview.layoutDoc._currentFrame = NumCast(activeFrame); } } @@ -423,7 +438,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + setTimeout(() => { + bestTarget._dataTransition = undefined; + }, transTime + 10); } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) { if (bestTarget.xRange !== activeItem.config_xRange) { @@ -565,7 +582,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.AddDocToList(bestTarget[DocData], layoutField, doc); } }); - setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10); + setTimeout( + () => + Array.from(transitioned).forEach( + action(doc => { + doc._dataTransition = undefined; + }) + ), + transTime + 10 + ); } if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) { const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); @@ -580,18 +605,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime, 2500); } - } else { - if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { - bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; - bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; - bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; - changed = true; - } + } else if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) { + bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX; + bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY; + bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale; + changed = true; } } if (changed) { return bestTargetView?.setViewTransition('all', transTime); } + return undefined; } /// copies values from the targetDoc (which is the prototype of the pinDoc) to @@ -628,8 +652,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; } if (pinProps.pinData.dataannos) { - const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); + const fieldKey = Doc.LayoutFieldKey(targetDoc); + pinDoc.config_annotations = new List<Doc>(DocListCast(targetDoc[DocData][fieldKey + '_annotations']).filter(doc => !doc.layout_unrendered)); } if (pinProps.pinData.inkable) { pinDoc.config_fillColor = targetDoc.fillColor; @@ -639,19 +663,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop; if (pinProps.pinData.clippable) { - const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.config_clipWidth = targetDoc[fkey + '_clipWidth']; + const fieldKey = Doc.LayoutFieldKey(targetDoc); + pinDoc.config_clipWidth = targetDoc[fieldKey + '_clipWidth']; } if (pinProps.pinData.datarange) { - pinDoc.config_xRange = undefined; //targetDoc?.xrange; - pinDoc.config_yRange = undefined; //targetDoc?.yrange; + pinDoc.config_xRange = undefined; // targetDoc?.xrange; + pinDoc.config_yRange = undefined; // targetDoc?.yrange; } if (pinProps.pinData.map) { // pinDoc.config_latitude = targetDoc?.latitude; // pinDoc.config_longitude = targetDoc?.longitude; pinDoc.config_map_zoom = targetDoc?.map_zoom; pinDoc.config_map_type = targetDoc?.map_type; - //... + // ... } if (pinProps.pinData.poslayoutview) pinDoc.config_pinLayoutData = new List<string>( @@ -711,8 +735,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * on the right. */ navigateToActiveItem = (afterNav?: () => void) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; + const { activeItem, targetDoc } = this; const finished = () => { afterNav?.(); targetDoc[Animation] = undefined; @@ -766,7 +789,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (targetDoc) { if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined; - DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { + DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, () => { // if target or the doc it annotates is not in the lightbox, then close the lightbox if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { LightboxView.Instance.SetLightboxDoc(undefined); @@ -798,7 +821,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { opacity = 0; } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { opacity = 1; - setTimeout(() => (tagDoc._dataTransition = undefined), 1000); + setTimeout(() => { + tagDoc._dataTransition = undefined; + }, 1000); } } const hidingIndAft = @@ -828,12 +853,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const savedStates = docs.map(doc => { switch (doc.type) { case DocumentType.COL: - if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; + if (doc._type_collection === CollectionViewType.Freeform) { + return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; + } break; case DocumentType.INK: if (doc.data instanceof InkField) { return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) }; } + break; + default: } return undefined; }); @@ -841,7 +870,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._exitTrail = () => { savedStates .filter(savedState => savedState) - .map(savedState => { + .forEach(savedState => { switch (savedState?.type) { case CollectionViewType.Freeform: { @@ -861,6 +890,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc.color = color; } break; + default: } }); LightboxView.Instance.SetLightboxDoc(undefined); @@ -879,8 +909,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - //The function that resets the presentation by removing every action done by it. It also - //stops the presentaton. + // The function that resets the presentation by removing every action done by it. It also + // stops the presentaton. resetPresentation = () => { this.childDocs .map(doc => PresBox.targetRenderedDoc(doc)) @@ -897,12 +927,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // The function allows for viewing the pres path on toggle @action togglePath = (off?: boolean) => { this._pathBoolean = off ? false : !this._pathBoolean; - CollectionFreeFormView.ShowPresPaths = this._pathBoolean; + SnappingManager.SetShowPresPaths(this._pathBoolean); }; // The function allows for expanding the view of pres on toggle @action toggleExpandMode = () => { - runInAction(() => (this._expandBoolean = !this._expandBoolean)); + runInAction(() => { + this._expandBoolean = !this._expandBoolean; + }); this.Document.expandBoolean = this._expandBoolean; this.childDocs.forEach(doc => { doc.presentation_expandInlineButton = this._expandBoolean; @@ -918,7 +950,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const startInd = NumCast(doc.presentation_indexedStart); this.progressivizedItems(doc) ?.slice(startInd) - .forEach(indexedDoc => (indexedDoc.opacity = 0)); + .forEach(indexedDoc => { + indexedDoc.opacity = 0; + }); doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); } // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; @@ -969,13 +1003,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { public static minimizedWidth = 198; public static OpenPresMinimized(doc: Doc, pt: number[]) { - doc.overlayX = pt[0]; - doc.overlayY = pt[1]; + [doc.overlayX, doc.overlayY] = pt; doc._height = 30; doc._width = PresBox.minimizedWidth; Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); - return (doc.presentation_status = PresStatus.Manual); + doc.presentation_status = PresStatus.Manual; + return doc.presentation_status; } /** @@ -984,12 +1018,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @undoBatch viewChanged = action((e: React.ChangeEvent) => { - //@ts-ignore - const type_collection = e.target.selectedOptions[0].value as CollectionViewType; - this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : ''); + const typeCollection = (e.target as any).selectedOptions[0].value as CollectionViewType; + this.layoutDoc.presFieldKey = this.fieldKey + (typeCollection === CollectionViewType.Tree ? '-linearized' : ''); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here - [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.Document._pivotField = undefined); - this.Document._type_collection = type_collection; + [CollectionViewType.Tree || CollectionViewType.Stacking].includes(typeCollection) && (this.Document._pivotField = undefined); + this.Document._type_collection = typeCollection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } @@ -1001,10 +1034,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ // @undoBatch mediaStopChanged = action((e: React.ChangeEvent) => { - const activeItem: Doc = this.activeItem; - //@ts-ignore - const stopDoc = e.target.selectedOptions[0].value as string; - const stopDocIndex: number = Number(stopDoc[0]); + const { activeItem } = this; + const stopDoc = (e.target as any).selectedOptions[0].value as string; + const stopDocIndex = Number(stopDoc[0]); activeItem.mediaStopDoc = stopDocIndex; if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) { const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); @@ -1025,10 +1057,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return StrCast(activeItem.presentation_movement); }); - whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isChildActive = isActive))); + whenChildContentsActiveChanged = action((isActive: boolean) => { + this._props.whenChildContentsActiveChanged((this._isChildActive = isActive)); + }); // For dragging documents into the presentation trail addDocumentFilter = (docs: Doc[]) => { - docs.forEach((doc, i) => { + const results = docs.map(doc => { if (doc.presentation_targetDoc) return true; if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); @@ -1041,13 +1075,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; } } else if (doc.type !== DocumentType.PRES) { + // eslint-disable-next-line operator-assignment if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; if (this._expandBoolean) doc.presentation_expandInlineButton = true; } + return false; }); - return true; + return !results.some(r => !r); }; childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); @@ -1068,24 +1104,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> <b> {index + 1}. {curDoc.title} </b> </div> ); - else if (tagDoc) + if (tagDoc) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> {index + 1}. {curDoc.title} </div> ); - else if (curDoc) + if (curDoc) return ( + // eslint-disable-next-line react/no-array-index-key <div key={index} className="selectedList-items"> {index + 1}. {curDoc.title} </div> ); + return null; }); } @@ -1095,15 +1135,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { presDocView && SelectionManager.SelectView(presDocView, false); }; - focusElement = (doc: Doc, options: FocusViewOptions) => { + focusElement = (doc: Doc) => { this.selectElement(doc); return undefined; }; - //Regular click + // Regular click @action selectElement = (doc: Doc, noNav = false) => { - CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.()); + CollectionStackedTimeline.CurrentlyPlaying?.map(clip => clip?.ComponentView?.Pause?.()); if (noNav) { const index = this.childDocs.indexOf(doc); if (index >= 0 && index < this.childDocs.length) { @@ -1115,7 +1155,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; - //Command click + // Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { if (!this.selectedArray.has(doc)) { @@ -1130,7 +1170,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectPres(); }; - //Shift click + // Shift click @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { this.clearSelectedArray(); @@ -1145,7 +1185,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectPres(); }; - //regular click + // regular click @action regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => { this.clearSelectedArray(); @@ -1176,9 +1216,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presentation_status === 'edit') { undoBatch( action(() => { - for (const doc of this.selectedArray) { - this.removeDocument(doc); - } + Array.from(this.selectedArray).forEach(doc => this.removeDocument(doc)); this.clearSelectedArray(); this._eleArray.length = 0; this._dragArray.length = 0; @@ -1245,8 +1283,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach(doc => this.addToSelectedArray(doc)); handled = true; } - default: break; + default: } if (handled) { e.stopPropagation(); @@ -1266,6 +1304,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const tagDoc = PresBox.targetRenderedDoc(doc); const srcContext = Cast(tagDoc.embedContainer, Doc, null); const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( + // eslint-disable-next-line react/no-array-index-key <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> <div className="pathOrder-frame">{index + 1}</div> </div> @@ -1298,7 +1337,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { order.push( <> {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)} - <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }}></div> + <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }} /> </> ); } @@ -1321,17 +1360,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection) .forEach((doc, index) => { const tagDoc = PresBox.targetRenderedDoc(doc); - if (tagDoc) { - const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; - const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.config_pinView) { - const n1x = NumCast(doc.config_panX); - const n1y = NumCast(doc.config_panY); - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } + const [n1x, n1y] = tagDoc // + ? [NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2, NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2] + : [NumCast(doc.config_panX), NumCast(doc.config_panY)]; + + if (index === 0) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; }); return ( <> @@ -1377,7 +1411,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch updateTransitionTime = (number: String, change?: number) => { - PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presentation_transition = timeInMS)), change); + PresBox.SetTransitionTime( + number, + (timeInMS: number) => + this.selectedArray.forEach(doc => { + doc.presentation_transition = timeInMS; + }), + change + ); }; // Converts seconds to ms and updates presentation_transition @@ -1387,7 +1428,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; - this.selectedArray.forEach(doc => (doc.config_zoom = scale)); + this.selectedArray.forEach(doc => { + doc.config_zoom = scale; + }); }; /* @@ -1399,76 +1442,96 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - this.selectedArray.forEach(doc => (doc.presentation_duration = timeInMS)); + this.selectedArray.forEach(doc => { + doc.presentation_duration = timeInMS; + }); }; @undoBatch - updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement))); + updateMovement = action((movement: PresMovement, all?: boolean) => + (all ? this.childDocs : this.selectedArray).forEach(doc => { + doc.presentation_movement = movement; + }) + ); @undoBatch updateHideBefore = (activeItem: Doc) => { activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore; - this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore)); + this.selectedArray.forEach(doc => { + doc.presentation_hideBefore = activeItem.presentation_hideBefore; + }); }; @undoBatch updateHide = (activeItem: Doc) => { activeItem.presentation_hide = !activeItem.presentation_hide; - this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide)); + this.selectedArray.forEach(doc => { + doc.presentation_hide = activeItem.presentation_hide; + }); }; @undoBatch updateHideAfter = (activeItem: Doc) => { activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter; - this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter)); + this.selectedArray.forEach(doc => { + doc.presentation_hideAfter = activeItem.presentation_hideAfter; + }); }; @undoBatch updateOpenDoc = (activeItem: Doc) => { activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; - this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); + this.selectedArray.forEach(doc => { + doc.presentation_openInLightbox = activeItem.presentation_openInLightbox; + }); }; @undoBatch updateEaseFunc = (activeItem: Doc) => { activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear'; - this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); + this.selectedArray.forEach(doc => { + doc.presEaseFunc = activeItem.presEaseFunc; + }); }; @undoBatch - updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect)); + updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => + (all ? this.childDocs : this.selectedArray).forEach(doc => { + doc.presentation_effectDirection = effect; + }); @undoBatch - updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); + updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => + (all ? this.childDocs : this.selectedArray).forEach(doc => { + bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect); + }); static _sliderBatch: any; static endBatch = () => { PresBox._sliderBatch.end(); document.removeEventListener('pointerup', PresBox.endBatch, true); }; - public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { - return ( - <input - type="range" - step={step} - min={min} - max={max} - value={value} - readOnly={true} - style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }} - className={`toolbar-slider ${active ? '' : 'none'}`} - onPointerDown={e => { - PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); - document.addEventListener('pointerup', PresBox.endBatch, true); - e.stopPropagation(); - }} - onChange={e => { - e.stopPropagation(); - change(e.target.value); - }} - /> - ); - }; + public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => ( + <input + type="range" + step={step} + min={min} + max={max} + value={value} + readOnly + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }} + className={`toolbar-slider ${active ? '' : 'none'}`} + onPointerDown={e => { + PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); + document.addEventListener('pointerup', PresBox.endBatch, true); + e.stopPropagation(); + }} + onChange={e => { + e.stopPropagation(); + change(e.target.value); + }} + /> + ); @undoBatch applyTo = (array: Doc[]) => { @@ -1476,17 +1539,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); - const { presentation_transition, presentation_duration, presentation_hideBefore, presentation_hideAfter } = this.activeItem; + // eslint-disable-next-line camelcase + const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; array.forEach(curDoc => { - curDoc.presentation_transition = presentation_transition; - curDoc.presentation_duration = presentation_duration; - curDoc.presentation_hideBefore = presentation_hideBefore; - curDoc.presentation_hideAfter = presentation_hideAfter; + curDoc.presentation_transition = pt; + curDoc.presentation_duration = pd; + curDoc.presentation_hideBefore = ph; + curDoc.presentation_hideAfter = pa; }); }; @computed get visibilityDurationDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0; @@ -1502,7 +1566,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Hide before </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> + <Tooltip title={<div className="dash-tooltip">Hide while presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} @@ -1511,7 +1575,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> + <Tooltip title={<div className="dash-tooltip">Hide after presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }} @@ -1520,7 +1584,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> + <Tooltip title={<div className="dash-tooltip">Open in lightbox view</div>}> <div className="ribbon-toggle" style={{ @@ -1550,15 +1614,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)} - <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> + <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> <div className="slider-text">Short</div> <div className="slider-text">Medium</div> <div className="slider-text">Long</div> @@ -1568,17 +1632,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get progressivizeDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None; - const bulletEffect = (effect: PresEffect) => ( + const bulletEffect = (presEffect: PresEffect) => ( <div - className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} + className={`presBox-dropdownOption ${activeItem.presentation_effect === presEffect || (presEffect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} onPointerDown={StopEvent} - onClick={() => this.updateEffect(effect, true)}> - {effect} + onClick={() => this.updateEffect(presEffect, true)}> + {presEffect} </div> ); return ( @@ -1598,12 +1663,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. let dataField = Doc.LayoutFieldKey(tagDoc); - if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; + if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations'; if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); }} - checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false} + checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined} /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> @@ -1612,7 +1677,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)} + onChange={() => { + activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1; + }} checked={!NumCast(activeItem.presentation_indexedStart)} /> </div> @@ -1622,7 +1689,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} + onChange={() => { + activeItem.presBulletExpand = !activeItem.presBulletExpand; + }} checked={BoolCast(activeItem.presBulletExpand)} /> </div> @@ -1642,14 +1711,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div - className={'presBox-dropdownOptions'} + className="presBox-dropdownOptions" style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} onPointerDown={e => e.stopPropagation()}> {Object.values(PresEffect) .filter(v => isNaN(Number(v))) - .map(effect => bulletEffect(effect))} + .map(peffect => bulletEffect(peffect))} </div> </div> </div> @@ -1659,7 +1728,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return null; } @computed get transitionDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; const preseEffect = (effect: PresEffect) => ( <div className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} @@ -1715,10 +1784,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {this.movementName(activeItem)} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} + id="presBoxMovementDropdown" onPointerDown={StopEvent} style={{ color: SnappingManager.userColor, @@ -1739,10 +1808,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> @@ -1750,19 +1819,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> + <FontAwesomeIcon icon="caret-up" /> </div> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + <FontAwesomeIcon icon="caret-down" /> </div> </div> </div> {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)} - <div className={'slider-headers'}> + <div className="slider-headers"> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> <div className="slider-text">Slow</div> @@ -1776,7 +1845,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + onChange={() => { + activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio); + }} checked={BoolCast(activeItem.presPlayAudio)} /> </div> @@ -1786,7 +1857,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + onChange={() => { + activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText); + }} checked={BoolCast(activeItem.presentation_zoomText)} /> </div> @@ -1800,13 +1873,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { color: SnappingManager.userColor, background: SnappingManager.userVariantColor, borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, - border: this._openEffectDropdown ? `solid 2px ${SettingsSnappingManagerManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, + border: this._openEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, }}> {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> <div className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} + id="presBoxMovementDropdown" style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor, @@ -1815,7 +1888,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerDown={e => e.stopPropagation()}> {Object.values(PresEffect) .filter(v => isNaN(Number(v))) - .map(effect => preseEffect(effect))} + .map(presEffect => preseEffect(presEffect))} </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> @@ -1840,20 +1913,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get mediaOptionsDropdown() { - const activeItem = this.activeItem; + const { activeItem } = this; if (activeItem && this.targetDoc) { const renderTarget = PresBox.targetRenderedDoc(this.activeItem); const clipStart = NumCast(renderTarget.clipStart); const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration'])); - const config_clipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); + const configClipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); return ( - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div> <div className="ribbon-box"> - Start {'&'} End Time - <div className={'slider-headers'}> + Start & End Time + <div className="slider-headers"> <div className="slider-block"> <div className="slider-text" style={{ fontWeight: 500 }}> Start time (s) @@ -1863,10 +1937,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" - readOnly={true} + readOnly value={NumCast(activeItem.config_clipStart).toFixed(2)} onKeyDown={e => e.stopPropagation()} - onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))} + onChange={action(e => { + activeItem.config_clipStart = Number(e.target.value); + })} /> </div> </div> @@ -1875,7 +1951,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Duration (s) </div> <div className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}> - {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} + {Math.round((configClipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} </div> </div> <div className="slider-block"> @@ -1888,9 +1964,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" - readOnly={true} - value={config_clipEnd.toFixed(2)} - onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))} + readOnly + value={configClipEnd.toFixed(2)} + onChange={action(e => { + activeItem.config_clipEnd = Number(e.target.value); + })} /> </div> </div> @@ -1901,7 +1979,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={config_clipEnd} + value={configClipEnd} style={{ gridColumn: 1, gridRow: 1, background: SnappingManager.userColor, color: SnappingManager.userVariantColor }} className={`toolbar-slider ${'end'}`} id="toolbar-slider" @@ -1909,7 +1987,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch = UndoManager.StartBatch('config_clipEnd'); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.backgroundColor = SnappingManager.userVariantColor; + endBlock.style.backgroundColor = SnappingManager.userVariantColor ?? ''; } e.stopPropagation(); }} @@ -1917,7 +1995,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.backgroundColor = SnappingManager.userBackgroundColor; + endBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? ''; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1938,7 +2016,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch = UndoManager.StartBatch('config_clipStart'); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.backgroundColor = SnappingManager.userVariantColor; + startBlock.style.backgroundColor = SnappingManager.userVariantColor ?? ''; } e.stopPropagation(); }} @@ -1946,7 +2024,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.backgroundColor = SnappingManager.userBackgroundColor; + startBlock.style.backgroundColor = SnappingManager.userBackgroundColor ?? ''; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1957,7 +2035,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="slider-headers"> <div className="slider-text">{clipStart.toFixed(2)} s</div> - <div className="slider-text"></div> + <div className="slider-text" /> <div className="slider-text">{clipEnd.toFixed(2)} s</div> </div> </div> @@ -1970,7 +2048,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" type="checkbox" style={{ border: `solid 1px ${SnappingManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStart = 'manual')} + onChange={() => { + activeItem.presentation_mediaStart = 'manual'; + }} checked={activeItem.presentation_mediaStart === 'manual'} /> <div>On click</div> @@ -1980,7 +2060,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" style={{ border: `solid 1px ${SnappingManager.userColor}` }} type="checkbox" - onChange={() => (activeItem.presentation_mediaStart = 'auto')} + onChange={() => { + activeItem.presentation_mediaStart = 'auto'; + }} checked={activeItem.presentation_mediaStart === 'auto'} /> <div>Automatically</div> @@ -1993,7 +2075,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" type="checkbox" style={{ border: `solid 1px ${SnappingManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStop = 'manual')} + onChange={() => { + activeItem.presentation_mediaStop = 'manual'; + }} checked={activeItem.presentation_mediaStop === 'manual'} /> <div>At media end time</div> @@ -2003,7 +2087,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-checkbox" type="checkbox" style={{ border: `solid 1px ${SnappingManager.userColor}` }} - onChange={() => (activeItem.presentation_mediaStop = 'auto')} + onChange={() => { + activeItem.presentation_mediaStop = 'auto'; + }} checked={activeItem.presentation_mediaStop === 'auto'} /> <div>On slide change</div> @@ -2031,6 +2117,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } + return undefined; } @computed get newDocumentToolbarDropdown() { return ( @@ -2094,9 +2181,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get newDocumentDropdown() { return ( - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="ribbon-box"> - Slide Title: <br></br> + Slide Title: <br /> <input className="ribbon-textInput" placeholder="..." @@ -2105,16 +2192,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onChange={e => { e.stopPropagation(); e.preventDefault(); - runInAction(() => (this.title = e.target.value)); - }}></input> + runInAction(() => { + this.title = e.target.value; + }); + }} + /> </div> <div className="ribbon-box"> Choose type: <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + <div + title="Text" + className="ribbon-toggle" + style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} + onClick={action(() => { + this.addFreeform = !this.addFreeform; + })}> Text </div> - <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}> + <div + title="Freeform" + className="ribbon-toggle" + style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} + onClick={action(() => { + this.addFreeform = !this.addFreeform; + })}> Freeform </div> </div> @@ -2122,23 +2224,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}> Preset layouts: <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} /> - <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}> + <div + className="layout" + style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'blank'; + })} + /> + <div + className="layout" + style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'title'; + })}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}> + <div + className="layout" + style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'header'; + })}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}> Section header </div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}> + <div + className="layout" + style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'content'; + })}> <div className="title" style={{ alignSelf: 'center' }}> Title </div> <div className="content">Text goes here</div> </div> - <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}> + <div + className="layout" + style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} + onClick={action(() => { + this.layout = 'twoColumns'; + })}> <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}> Title </div> @@ -2150,8 +2278,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}> - <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} /> + <div + className="open-layout" + onClick={action(() => { + this.openLayouts = !this.openLayouts; + })}> + <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="caret-down" size="lg" /> </div> </div> <div className="ribbon-final-box"> @@ -2166,17 +2298,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } createNewSlide = (layout?: string, title?: string, freeform?: boolean) => { - let doc = undefined; + let doc; if (layout) doc = this.createTemplate(layout); if (freeform && layout) doc = this.createTemplate(layout, title); if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title }); if (doc) { const tabMap = CollectionDockingView.Instance?.tabMap; - const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc; - const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? tab; + const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc; + const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab; const data = Cast(presCollection?.data, listSpec(Doc)); - const config_data = Cast(this.Document.data, listSpec(Doc)); - if (data && config_data) { + const configData = Cast(this.Document.data, listSpec(Doc)); + if (data && configData) { data.push(doc); this._props.pinToPres(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); @@ -2198,12 +2330,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' }); // prettier-ignore switch (layout) { - case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); - case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) + case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y }); + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input || 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input || 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input || 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input || 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) + default: } + return undefined; }; // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) @@ -2246,17 +2380,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - toggleProperties = () => (SnappingManager.Instance.propertiesWidth = SnappingManager.Instance.propertiesWidth > 0 ? 0 : 250); + toggleProperties = () => { + SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250; + }; @computed get toolbar() { - const propIcon = SnappingManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; - const propTitle = SnappingManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; + const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.Document._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; const activeColor = SnappingManager.userVariantColor; const inactiveColor = lightOrDark(SnappingManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SnappingManager.userBackgroundColor; return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.Document) ? null : ( - <div id="toolbarContainer" className={'presBox-toolbar'}> + <div id="toolbarContainer" className="presBox-toolbar"> {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> <FontAwesomeIcon icon={"plus"} /> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> @@ -2266,7 +2402,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} className="toolbar-button" onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}> - <FontAwesomeIcon icon={'exchange-alt'} /> + <FontAwesomeIcon icon="exchange-alt" /> </div> </Tooltip> {isMini ? null : ( @@ -2274,12 +2410,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="toolbar-divider" /> <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}> <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className="toolbar-thumbtack" icon="keyboard" style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SnappingManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} /> + <FontAwesomeIcon className="toolbar-thumbtack" icon={propIcon} style={{ color: SettingsManager.Instance.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -2324,7 +2460,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.gotoDocument(this.itemIndex, this.activeItem); } })}> - <FontAwesomeIcon icon={'play-circle'} /> + <FontAwesomeIcon icon="play-circle" /> <div style={{ display: this._props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}> Present</div> </div> {mode === CollectionViewType.Carousel3D || isMini ? null : ( @@ -2333,7 +2469,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={action(() => { if (this.childDocs.length) this._presentTools = !this._presentTools; })}> - <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} /> + <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon="angle-down" /> {this.presentDropdown} </div> )} @@ -2396,7 +2532,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { false ) }> - <FontAwesomeIcon icon={'arrow-left'} /> + <FontAwesomeIcon icon="arrow-left" /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}> @@ -2424,10 +2560,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { false ) }> - <FontAwesomeIcon icon={'arrow-right'} /> + <FontAwesomeIcon icon="arrow-right" /> </div> - <div className="presPanel-divider"></div> - <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> + <div className="presPanel-divider" /> + <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} @@ -2451,7 +2587,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {inOverlay ? '' : 'Slide'} {this.itemIndex + 1} {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> - <div className="presPanel-divider"></div> + <div className="presPanel-divider" /> {this._props.PanelWidth() > 250 ? ( <div className="presPanel-button-text" @@ -2465,7 +2601,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ) : ( <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}> - <FontAwesomeIcon icon={'times'} /> + <FontAwesomeIcon icon="times" /> </div> )} </div> @@ -2480,7 +2616,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @action - prevClicked = (e: PointerEvent) => { + prevClicked = () => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); @@ -2489,7 +2625,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @action - nextClicked = (e: PointerEvent) => { + nextClicked = () => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); @@ -2505,7 +2641,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { AddToMap = (treeViewDoc: Doc, index: number[]) => { if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. - var indexNum = 0; + let indexNum = 0; for (let i = 0; i < index.length; i++) { indexNum += index[i] * 10 ** -i; } @@ -2517,19 +2653,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs } } + return undefined; }; SlideIndex = (slideDoc: Doc) => DocListCast(this.dataDoc[this.presFieldKey]).indexOf(slideDoc); - RemFromMap = (treeViewDoc: Doc, index: number[]) => { + RemFromMap = (treeViewDoc: Doc) => { if (!treeViewDoc.presentation_targetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. if (!this._unmounting && this.isTree) { this._treeViewMap.delete(treeViewDoc); this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap)); } + return undefined; }; - sort = (treeView_Map: Map<Doc, number>) => [...treeView_Map.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); + sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); render() { // needed to ensure that the childDocs are loaded for looking up fields @@ -2541,21 +2679,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player - <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> + <div + className="miniPres" + onClick={e => e.stopPropagation()} + onPointerEnter={action(() => { + this._forceKeyEvents = true; + })}> <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: Doc.ActivePresentation === this.Document ? 'green' : '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> - <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}> + <Tooltip title={<div className="dash-tooltip">Loop</div>}> <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} - onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}> - <FontAwesomeIcon icon={'redo-alt'} /> + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + () => { + this.layoutDoc.presLoop = !this.layoutDoc.presLoop; + }, + false, + false + ) + }> + <FontAwesomeIcon icon="redo-alt" /> </div> </Tooltip> - <div className="presPanel-divider"></div> + <div className="presPanel-divider" /> <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}> - <FontAwesomeIcon icon={'arrow-left'} /> + <FontAwesomeIcon icon="arrow-left" /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}> @@ -2563,10 +2718,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}> - <FontAwesomeIcon icon={'arrow-right'} /> + <FontAwesomeIcon icon="arrow-right" /> </div> - <div className="presPanel-divider"></div> - <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> + <div className="presPanel-divider" /> + <Tooltip title={<div className="dash-tooltip">Click to return to 1st slide</div>}> <div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}> <b>1</b> </div> @@ -2590,15 +2745,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="Slide"> {mode !== CollectionViewType.Invalid ? ( <CollectionView + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} PanelWidth={this._props.PanelWidth} PanelHeight={this.panelHeight} - childIgnoreNativeSize={true} + childIgnoreNativeSize moveDocument={returnFalse} - ignoreUnrendered={true} + ignoreUnrendered childDragAction={dropActionType.move} setContentViewBox={emptyFunction} - //childLayoutFitWidth={returnTrue} + // childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} @@ -2629,6 +2785,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) { PresBox.NavigateToTarget(bestTarget, activeItem); }); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index fca5a2770..5fa32ad12 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,14 +1,16 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; -import { returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -20,9 +22,9 @@ import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; -import { DocumentView } from '../../nodes/DocumentView'; -import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { StyleProp } from '../../StyleProvider'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; @@ -72,7 +74,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // computes index of this presentation slide in the presBox list @computed get indexInPres() { - return this.presBoxView?.SlideIndex(this.slideDoc); + return this.presBoxView?.SlideIndex(this.slideDoc) ?? 0; } @computed get selectedArray() { @@ -87,7 +89,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( () => ({ expand: this.slideDoc.presentation_expandInlineButton, height: this.collapsedHeight }), - ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), + ({ expand, height }) => { + this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0); + }, { fireImmediately: true } ); } @@ -95,12 +99,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this._heightDisposer?.(); } - presExpandDocumentClick = () => (this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton); + presExpandDocumentClick = () => { + this.slideDoc.presentation_expandInlineButton = !this.slideDoc.presentation_expandInlineButton; + }; embedHeight = () => this.collapsedHeight + this.expandViewHeight; embedWidth = () => this._props.PanelWidth() / 2; - styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { - return property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property); - }; + // prettier-ignore + styleProvider = ( doc: Doc | undefined, props: Opt<FieldViewProps>, property: string ): any => + (property === StyleProp.Opacity ? 1 : this._props.styleProvider?.(doc, props, property)); /** * The function that is responsible for rendering a preview or not for this * presentation element. @@ -114,7 +120,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { PanelHeight={this.embedHeight} isContentActive={this._props.isContentActive} styleProvider={this.styleProvider} - hideLinkButton={true} + hideLinkButton ScreenToLocalTransform={Transform.Identity} renderDepth={this._props.renderDepth + 1} containerViewPath={returnEmptyDoclist} @@ -151,7 +157,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ref={this._titleRef} editing={undefined} contents={doc.title} - overflow={'ellipsis'} + overflow="ellipsis" GetValue={() => StrCast(doc.title)} SetValue={(value: string) => { doc.title = !value.trim().length ? '-untitled-' : value; @@ -178,10 +184,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) { this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); - setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => { - e.stopPropagation(); - e.preventDefault(); - this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + this.presBoxView?.modifierSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, clickEv.shiftKey || clickEv.ctrlKey || clickEv.metaKey, clickEv.ctrlKey || clickEv.metaKey, clickEv.shiftKey); this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem); }); } @@ -210,7 +216,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (dragArray.length >= 1) { const doc = document.createElement('div'); doc.className = 'presItem-multiDrag'; - doc.innerText = 'Move ' + this.selectedArray?.size + ' slides'; + doc.innerText = 'Move ' + (this.selectedArray?.size ?? 0) + ' slides'; doc.style.position = 'absolute'; doc.style.top = e.clientY + 'px'; doc.style.left = e.clientX - 50 + 'px'; @@ -218,7 +224,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (activeItem) { - runInAction(() => (this._dragging = true)); + runInAction(() => { + this._dragging = true; + }); DragManager.StartDocumentDrag( dragItem.map(ele => ele), dragData, @@ -226,7 +234,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.clientY, undefined, action(() => { - Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); + Array.from(classesToRestore).forEach(pair => { + // eslint-disable-next-line prefer-destructuring + pair[0].className = pair[1]; + }); this._dragging = false; }) ); @@ -235,7 +246,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; }; - onPointerOver = (e: any) => { + onPointerOver = () => { document.removeEventListener('pointermove', this.onPointerMove); document.addEventListener('pointermove', this.onPointerMove); }; @@ -245,7 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); - const y = e.clientY - rect.top; //y position within the element. + const y = e.clientY - rect.top; // y position within the element. const height = slide.clientHeight; const halfLine = height / 2; if (y <= halfLine) { @@ -259,7 +270,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { document.removeEventListener('pointermove', this.onPointerMove); }; - onPointerLeave = (e: any) => { + onPointerLeave = () => { const slide = this._itemRef.current; if (slide) { slide.style.borderTop = '0px'; @@ -341,7 +352,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; hideRecording = undoable( - action((e: React.MouseEvent, iconClick: boolean = false) => { + action((e: React.MouseEvent) => { e.stopPropagation(); this.removeAllRecordingInOverlay(); }), @@ -396,7 +407,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { lfg = (e: React.MouseEvent) => { e.stopPropagation(); // TODO: fix this bug - const { toggleChildrenRun } = this.slideDoc; + // const { toggleChildrenRun } = this.slideDoc; TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); // call this.slideDoc.recurChildren() to get all the children @@ -407,15 +418,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { get toolbarWidth(): number { const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); const width = NumCast(this.presBox?._width); - return presBoxDocView ? presBoxDocView._props.PanelWidth() : width ? width : 300; + return presBoxDocView ? presBoxDocView._props.PanelWidth() : width || 300; } @computed get presButtons() { - const presBox = this.presBox; + const { presBox, targetDoc, slideDoc: activeItem } = this; const presBoxColor = StrCast(presBox?._backgroundColor); const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc = this.targetDoc; - const activeItem = this.slideDoc; const hasChildren = BoolCast(this.slideDoc?.hasChildren); const items: JSX.Element[] = []; @@ -442,7 +451,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); items.push( <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> - <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> + <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> @@ -462,7 +471,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }> <div className="slideButton" - onClick={() => (activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3)} + onClick={() => { + activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_groupWithUp) + 1) % 3; + }} style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, @@ -472,7 +483,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined, }}> <div style={{ transform: activeItem.presentation_groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> - <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon="arrow-up" onPointerDown={e => e.stopPropagation()} /> </div> </div> </Tooltip> @@ -501,15 +512,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this.lfg(e); }} style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={'circle-play'} onPointerDown={e => e.stopPropagation()} /> + <FontAwesomeIcon icon="circle-play" onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); } items.push( <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> - <div className={'slideButton'} onClick={this.removePresentationItem}> - <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> + <div className="slideButton" onClick={this.removePresentationItem}> + <FontAwesomeIcon icon="trash" onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> ); @@ -517,18 +528,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get mainItem() { - const isSelected: boolean = this.selectedArray?.has(this.slideDoc) ? true : false; + const { presBox, slideDoc: activeItem } = this; + const isSelected: boolean = !!this.selectedArray?.has(activeItem); const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres; const miniView: boolean = this.toolbarWidth <= 110; - const presBox = this.presBox; //presBox const presBoxColor: string = StrCast(presBox?._backgroundColor); const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const activeItem: Doc = this.slideDoc; return ( <div className="presItem-container" - key={this.slideDoc[Id] + this.indexInPres} + key={activeItem[Id] + this.indexInPres} ref={this._itemRef} style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', @@ -538,9 +548,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding), paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding), }} - onDoubleClick={action(e => { + onDoubleClick={action(() => { this.toggleProperties(); - this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, false); + this.presBoxView?.regularSelect(activeItem, this._itemRef.current!, this._dragRef.current!, false); })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} @@ -552,11 +562,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ) : ( <div ref={this._dragRef} - className={`presItem-slide ${isCurrent ? 'active' : ''}${this.slideDoc.runProcess ? ' testingv2' : ''}`} + className={`presItem-slide ${isCurrent ? 'active' : ''}${activeItem.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', backgroundColor: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor), - //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, + // layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, }}> <div @@ -564,7 +574,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'inline-flex', pointerEvents: isSelected ? undefined : 'none', - width: `calc(100% ${this.slideDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, + width: `calc(100% ${activeItem.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, cursor: isSelected ? 'text' : 'grab', }}> <div @@ -577,7 +587,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }} onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div> - <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> + <EditableView ref={this._titleRef} oneLine editing={!isSelected ? false : undefined} contents={activeItem.title} overflow="ellipsis" GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index a1f5ce703..053c88e17 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -62,7 +62,7 @@ class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps> pinToPres = () => this._props.pinToPres(this.annoTextRegion, {}); @undoBatch - makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle); + makeTargetToggle = () => { this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle }; // prettier-ignore isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle); @undoBatch @@ -80,7 +80,7 @@ class RegionAnnotation extends ObservableReactComponent<IRegionAnnotationProps> AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this); AnchorMenu.Instance.Pinned = false; AnchorMenu.Instance.PinToPres = this.pinToPres; - AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle; + AnchorMenu.Instance.MakeTargetToggle = this.makeTargetToggle; AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler; AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index cd13d4cbc..4362c4fbb 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -294,7 +295,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { render() { return ( <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} + {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : null} </div> ); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index af9f05a14..0c1a419aa 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -40,6 +42,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + // eslint-disable-next-line no-use-before-define public static Instance: SearchBox; private _inputRef = React.createRef<HTMLInputElement>(); @@ -135,20 +138,23 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * This method iterates asynchronously through an array of docs and all docs within those * docs, calling the function func on each doc. */ - static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) { + static async foreachRecursiveDocAsync(docsIn: Doc[], func: (depth: number, doc: Doc) => void) { + let docs = docsIn; let newarray: Doc[] = []; - var depth = 0; + let depth = 0; while (docs.length > 0) { newarray = []; + // eslint-disable-next-line no-await-in-loop await Promise.all( docs .filter(d => d) + // eslint-disable-next-line no-loop-func .map(async d => { const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - const docs = await DocListCastAsync(data); - docs && newarray.push(...docs); + const dataDocs = await DocListCastAsync(data); + dataDocs && newarray.push(...dataDocs); func(depth, d); }) ); @@ -170,6 +176,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { case DocumentType.IMG : return 'Img'; case DocumentType.RTF : return 'Rtf'; case DocumentType.COL : return 'Col:'+colType.substring(0,3); + default: } // prettier-ignore return type.charAt(0).toUpperCase() + type.substring(1, 3); @@ -298,35 +305,27 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { if (query) { this.searchCollection(query); const response = await fetchRecommendations('', query, [], true); - const recs = response.recommendations; + const recs = response.recommendations as any[]; const recommendations: IRecommendation[] = []; - for (const key in recs) { - const title = recs[key].title; - const url = recs[key].url; - const type = recs[key].type; - const text = recs[key].text; - const transcript = recs[key].transcript; - const previewUrl = recs[key].previewUrl; - const embedding = recs[key].embedding; - const distance = recs[key].distance; - const source = recs[key].source; - const related_concepts = recs[key].related_concepts; - const docId = recs[key].doc_id; + recs.forEach(rec => { + const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec; recommendations.push({ - title: title, + title, data: url, - type: type, - text: text, - transcript: transcript, - previewUrl: previewUrl, - embedding: embedding, + type, + text, + transcript, + previewUrl, + embedding, distance: Math.round(distance * 100) / 100, source: source, - related_concepts: related_concepts, - docId: docId, + related_concepts: relatedConcepts, + docId, }); - } - const setRecommendations = action(() => (this._recommendations = recommendations)); + }); + const setRecommendations = action(() => { + this._recommendations = recommendations; + }); setRecommendations(); } }; @@ -375,30 +374,30 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * This method renders the search input box, select drop-down menu, and search results. */ render() { - var validResults = 0; + let validResults = 0; const isLinkSearch: boolean = this._props.linkSearch; const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank - const resultsJSX = Array(); + const resultsJSX = [] as any[]; const fromDoc = this._props.linkFrom?.(); sortedResults.forEach(result => { - var className = 'searchBox-results-scroll-view-result'; + let className = 'searchBox-results-scroll-view-result'; if (this._selectedResult === result[0]) { className += ' searchBox-results-scroll-view-result-selected'; } const formattedType = SearchBox.formatType(StrCast(result[0].type), StrCast(result[0].type_collection)); - const title = result[0].title; + const { title } = result[0]; if (this._docTypeString === 'keys' || this._docTypeString === 'all' || this._docTypeString === result[0].type) { validResults++; resultsJSX.push( - <Tooltip key={result[0][Id]} placement={'right'} title={<div className="dash-tooltip">{title as string}</div>}> + <Tooltip key={result[0][Id]} placement="right" title={<div className="dash-tooltip">{title as string}</div>}> <div onClick={ isLinkSearch @@ -429,6 +428,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } }); + // eslint-disable-next-line react/jsx-props-no-spreading const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />); return ( @@ -459,7 +459,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { <div className="searchBox-results-container"> <div className="section-header" style={{ background: SettingsManager.userVariantColor }}> <div className="section-title">Results</div> - <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div> </div> <div className="searchBox-results-view">{resultsJSX}</div> </div> @@ -468,7 +468,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { <div className="searchBox-recommendations-container"> <div className="section-header" style={{ background: SettingsManager.userVariantColor }}> <div className="section-title">Recommendations</div> - <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + <div className="section-subtitle">{`${validResults} result` + (validResults === 1 ? '' : 's')}</div> </div> <div className="searchBox-recommendations-view">{recommendationsJSX}</div> </div> diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index b87e5cdde..1ab0932a3 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -36,6 +36,7 @@ import { dropActionType } from '../../util/DropActionTypes'; */ @observer export class TopBar extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define static Instance: TopBar; @observable private _flipDocumentation = 0; constructor(props: any) { @@ -45,10 +46,12 @@ export class TopBar extends ObservableReactComponent<{}> { } navigateToHome = () => { - (CollectionDockingView.Instance?.CaptureThumbnail() ?? new Promise<void>(res => res())).then(() => { + (CollectionDockingView.Instance?.CaptureThumbnail() ?? + new Promise<void>(res => { res(); })) .then(() => + { Doc.ActivePage = 'home'; DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use - }); + }); // prettier-ignore }; @computed get color() { @@ -62,7 +65,9 @@ export class TopBar extends ObservableReactComponent<{}> { } @observable happyHeart: boolean = PingManager.Instance.IsBeating; - setHappyHeart = action((status: boolean) => (this.happyHeart = status)); + setHappyHeart = action((status: boolean) => { + this.happyHeart = status; + }); dispose = reaction( () => PingManager.Instance.IsBeating, isBeating => this.setHappyHeart(isBeating) @@ -86,7 +91,7 @@ export class TopBar extends ObservableReactComponent<{}> { /> ) : ( <div className="logo-container"> - <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img> + <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo" /> <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, fontWeight: 200 }}>brown</span> <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_BLUE : Colors.MEDIUM_BLUE, fontWeight: 500 }}>dash</span> </div> @@ -193,11 +198,11 @@ export class TopBar extends ObservableReactComponent<{}> { }} /> ) : null} - <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> + <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> <Flip key={this._flipDocumentation}> - <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> + <IconButton tooltip="Documentation ⌘D" size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> </Flip> - <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} /> + <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} /> <IconButton size={Size.SMALL} onClick={ServerStats.Instance.open} @@ -214,11 +219,13 @@ export class TopBar extends ObservableReactComponent<{}> { /** * Make the documentation icon flip around to draw attention to it. */ - FlipDocumentationIcon = action(() => (this._flipDocumentation = this._flipDocumentation + 1)); + FlipDocumentationIcon = action(() => { + this._flipDocumentation += 1; + }); render() { return ( - //TODO:glr Add support for light / dark mode + // TODO:glr Add support for light / dark mode <div style={{ pointerEvents: 'all', diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7714ce46d..4512d5c5b 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -7,7 +7,7 @@ import { DocServer } from '../client/DocServer'; import { CollectionViewType, DocumentType } from '../client/documents/DocumentTypes'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; -import { undoable } from '../client/util/UndoManager'; +import { undoable, UndoManager } from '../client/util/UndoManager'; import { ClientUtils, incrementTitleCopy } from '../ClientUtils'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks, @@ -38,7 +38,7 @@ export namespace Field { */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string { const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); - const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const valFunc = (field: FieldType): string => { const res = field instanceof ComputedField && showComputedValue @@ -55,7 +55,7 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(field); + return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield); } export function toScriptString(field: FieldType) { switch (typeof field) { @@ -98,9 +98,11 @@ export namespace Field { export function IsField(field: any, includeUndefined: boolean = false): field is FieldType | undefined { return ['string', 'number', 'boolean'].includes(typeof field) || field instanceof ObjectField || field instanceof RefField || (includeUndefined && field === undefined); } + // eslint-disable-next-line @typescript-eslint/no-shadow export function Copy(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; } + UndoManager.SetFieldPrinter(toJavascriptString); } export type FieldType = number | string | boolean | ObjectField | RefField; export type Opt<T> = T | undefined; @@ -369,7 +371,7 @@ export class Doc extends RefField { const sameAuthor = this.author === ClientUtils.CurrentUserEmail(); const fprefix = 'fields.'; Object.keys(set ?? {}) - .filter(key => Object.prototype.hasOwnProperty.call(set, key) && key.startsWith(fprefix)) + .filter(key => key.startsWith(fprefix)) .forEach(async key => { const fKey = key.substring(fprefix.length); const fn = async () => { @@ -396,7 +398,7 @@ export class Doc extends RefField { }); const unset = diff.$unset; Object.keys(unset ?? {}) - .filter(key => Object.prototype.hasOwnProperty.call(unset, key) && key.startsWith(fprefix)) + .filter(key => key.startsWith(fprefix)) .forEach(async key => { const fKey = key.substring(7); const fn = () => { @@ -511,15 +513,13 @@ export namespace Doc { */ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<FieldType>>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); - Object.keys(fields) - .filter(key => Object.prototype.hasOwnProperty.call(fields, key)) - .forEach(key => { - const value = (fields as any)[key]; - if (!skipUndefineds || value !== undefined) { - // Do we want to filter out undefineds? - doc[key] = value; - } - }); + Object.keys(fields).forEach(key => { + const value = (fields as any)[key]; + if (!skipUndefineds || value !== undefined) { + // Do we want to filter out undefineds? + doc[key] = value; + } + }); isInitializing && (doc[Initializing] = false); return doc; } @@ -640,7 +640,7 @@ export namespace Doc { export function BestEmbedding(doc: Doc) { const dataDoc = doc[DocData]; const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail()); + const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(d => !d.embedContainer && d.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -673,27 +673,27 @@ export namespace Doc { }; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = async (field: ObjectField) => { + const copyObjectField = async (objField: ObjectField) => { const list = await Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates))); assignKey(new List<Doc>(clones)); } else { - assignKey(ObjectField.MakeCopy(field)); - if (field instanceof RichTextField) { - if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) { + assignKey(ObjectField.MakeCopy(objField)); + if (objField instanceof RichTextField) { + if (DocsInTextFieldIds.some(id => objField.Data.includes(`"${id}":`))) { const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g'); - const rawdocids = field.Data.match(docidsearch); + const rawdocids = objField.Data.match(docidsearch); const docids = rawdocids?.map((str: string) => DocsInTextFieldIds.reduce((output, exp) => output.replace(new RegExp(`${exp}":`, 'g'), ''), str) .replace(/"/g, '') .trim() ); const results = docids && (await DocServer.GetRefFields(docids)); - const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key])); - docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); - rtfs.push({ copy, key, field }); + const rdocs = results && Array.from(Object.keys(results)).map(rkey => DocCast(results[rkey])); + rdocs?.map(d => d && Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); + rtfs.push({ copy, key, field: objField }); } } } @@ -769,7 +769,7 @@ export namespace Doc { export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); + const clone = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); linkedDocs.forEach(link => Doc.AddLink?.(link, true)); @@ -787,8 +787,8 @@ export namespace Doc { copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; - clonedDocs.forEach(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired)); - return { clone: copy, map: cloneMap, linkMap }; + clonedDocs.forEach(cloneDoc => Doc.repairClone(cloneDoc, cloneMap, cloneTemplates, repaired)); + return { clone, map: cloneMap, linkMap }; } const _pendingMap = new Set<string>(); @@ -1236,9 +1236,9 @@ export namespace Doc { }); } /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted - export function UnHighlightDoc(doc?: Doc) { + export function UnHighlightDoc(docs?: Doc) { runInAction(() => { - (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => { + (docs ? [docs] : Array.from(highlightedDocs)).forEach(doc => { highlightedDocs.delete(doc); highlightedDocs.delete(doc[DocData]); doc[Highlight] = doc[DocData][Highlight] = false; diff --git a/src/fields/List.ts b/src/fields/List.ts index f6e0473ea..f97f208fe 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -2,14 +2,12 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { alias, list, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; -import { Field, FieldType } from './Doc'; +import { Field, FieldType, StrListCast } from './Doc'; import { FieldTuples, Self, SelfProxy } from './DocSymbols'; import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjGetRefFields, ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; -import { listSpec } from './Schema'; -import { Cast } from './Types'; import { containedFieldChangedHandler, deleteProperty, getter, setter } from './util'; function toObjectField(field: FieldType) { @@ -223,7 +221,7 @@ class ListImpl<T extends FieldType> extends ObjectField { }, }; static listGetter(target: any, prop: string | symbol, receiver: any): any { - if (ListImpl.listHandlers.hasOwnProperty(prop)) { + if (Object.prototype.hasOwnProperty.call(ListImpl.listHandlers, prop)) { return ListImpl.listHandlers[prop]; } return getter(target, prop, receiver); @@ -252,6 +250,7 @@ class ListImpl<T extends FieldType> extends ObjectField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); + // eslint-disable-next-line no-use-before-define this[SelfProxy] = list as any as List<FieldType>; // bcz: ugh .. don't know how to convince typesecript that list is a List if (fields) { this[SelfProxy].push(...fields); @@ -287,13 +286,13 @@ class ListImpl<T extends FieldType> extends ObjectField { private set __fieldTuples(value) { this[FieldTuples] = value; - for (const key in value) { - const item = value[key]; + Object.keys(value).forEach(key => { + const item = value[Number(key)]; if (item instanceof ObjectField) { item[Parent] = this[Self]; item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item); } - } + }); } [Copy]() { @@ -306,25 +305,24 @@ class ListImpl<T extends FieldType> extends ObjectField { @observable private [FieldTuples]: StoredType<T>[] = []; private [Self] = this; + // eslint-disable-next-line no-use-before-define private [SelfProxy]: List<FieldType>; // also used in utils.ts even though it won't be found using find all references - [ToJavascriptString]() { - return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; - } - [ToScriptString]() { - return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; - } - [ToString]() { - return `[${(this as any).map((field: any) => Field.toString(field))}]`; - } + [ToScriptString]() { return `new List(${this[ToJavascriptString]})`; } // prettier-ignore + [ToJavascriptString]() { return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; } // prettier-ignore + [ToString]() { return `[${(this as any).map((field: any) => Field.toString(field))}]`; } // prettier-ignore } + +// declare List as a type so you can use it in type declarations, e.g., { l: List, ...} export type List<T extends FieldType> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; +// decalre List as a value so you can invoke 'new' on it, e.g., new List<Doc>() +// eslint-disable-next-line no-redeclare export const List: { new <T extends FieldType>(fields?: T[]): List<T> } = ListImpl as any; ScriptingGlobals.add('List', List); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function compareLists(l1: any, l2: any) { - const L1 = Cast(l1, listSpec('string'), []); - const L2 = Cast(l2, listSpec('string'), []); + const L1 = StrListCast(l1); + const L2 = StrListCast(l2); return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true); }, 'compare two lists'); diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index 6c70adc1d..231086262 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -5,6 +5,7 @@ import { RefField } from './RefField'; export abstract class ObjectField { // prettier-ignore public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; + // eslint-disable-next-line no-use-before-define items: FieldType[] | undefined; length: number | undefined; hint?: any }, serverOp?: any) => void; diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 820d9b6ff..4f8058ce4 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -21,7 +21,7 @@ export class ProxyField<T extends RefField> extends ObjectField { constructor(value?: T | string) { super(); if (typeof value === 'string') { - //this.cache = DocServer.GetCachedRefField(value) as any; + // this.cache = DocServer.GetCachedRefField(value) as any; this.fieldId = value; } else if (value) { this.cache = { field: value, p: undefined }; @@ -29,7 +29,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } } - [ToValue](doc: any) { + [ToValue](/* doc: any */) { return ProxyField.toValue(this); } @@ -39,10 +39,10 @@ export class ProxyField<T extends RefField> extends ObjectField { } [ToJavascriptString]() { - return Field.toScriptString(this[ToValue](undefined)?.value); + return Field.toScriptString(this[ToValue]()?.value); } [ToScriptString]() { - return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? + return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } [ToString]() { return 'ProxyField'; @@ -59,7 +59,9 @@ export class ProxyField<T extends RefField> extends ObjectField { return this._cache; } private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) { - runInAction(() => (this._cache = { ...val })); + runInAction(() => { + this._cache = { ...val }; + }); } private failed = false; @@ -78,7 +80,7 @@ export class ProxyField<T extends RefField> extends ObjectField { return this.cache.field ?? this.cache.p; } @computed get needsRequesting(): boolean { - return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false; + return !!(!this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId)); } setExternalValuePromise(externalValuePromise: Promise<any>) { @@ -92,6 +94,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } } +// eslint-disable-next-line no-redeclare export namespace ProxyField { let useProxy = true; export function DisableProxyFields() { @@ -115,9 +118,11 @@ export namespace ProxyField { if (useProxy) { return { value: value.value }; } + return undefined; } } +// eslint-disable-next-line no-use-before-define function prefetchValue(proxy: PrefetchProxy<RefField>) { return proxy.value as any; } diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 364899dc7..ed603e5de 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -36,7 +36,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu if (prop in schema) { const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) { - //defaultSpec + // defaultSpec return Cast(field, desc.type, desc.defaultVal); } if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { diff --git a/src/fields/Types.ts b/src/fields/Types.ts index 6ed94d341..57a310f6d 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -7,6 +7,7 @@ import { RichTextField } from './RichTextField'; import { ScriptField } from './ScriptField'; import { CsvField, ImageField, WebField } from './URLField'; +// eslint-disable-next-line no-use-before-define export type ToConstructor<T extends FieldType> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T; export type DefaultFieldConstructor<T extends FieldType> = { @@ -27,6 +28,7 @@ export type ToType<T extends InterfaceValue> = T extends 'string' : T extends ListSpec<infer U> ? List<U> : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never; + // eslint-disable-next-line @typescript-eslint/no-unused-vars T extends DefaultFieldConstructor<infer _U> ? never : T extends { new (...args: any[]): List<FieldType> } @@ -37,6 +39,10 @@ export type ToType<T extends InterfaceValue> = T extends 'string' ? R : never; +export interface Interface { + [key: string]: InterfaceValue; + // [key: string]: ToConstructor<Field> | ListSpec<Field[]>; +} export type ToInterface<T extends Interface> = { [P in Exclude<keyof T, 'proto'>]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>; }; @@ -47,10 +53,6 @@ export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never; export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : []; export type HasTail<T extends any[]> = T extends [] | [any] ? false : true; // TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial -export interface Interface { - [key: string]: InterfaceValue; - // [key: string]: ToConstructor<Field> | ListSpec<Field[]>; -} export type WithoutRefField<T extends FieldType> = T extends RefField ? never : T; export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>; @@ -58,13 +60,16 @@ export type CastCtor = ToConstructor<FieldType> | ListSpec<FieldType>; type WithoutList<T extends FieldType> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; +// eslint-disable-next-line no-redeclare export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; +// eslint-disable-next-line no-redeclare export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined { if (field instanceof Promise) { return defaultVal === undefined ? (field.then(f => Cast(f, ctor) as any) as any) : defaultVal === null ? undefined : defaultVal; } if (field !== undefined && !(field instanceof Promise)) { if (typeof ctor === 'string') { + // eslint-disable-next-line valid-typeof if (typeof field === ctor) { return field as ToType<T>; } @@ -118,7 +123,9 @@ export function ImageCast(field: FieldResult, defaultVal: ImageField | null = nu } export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; +// eslint-disable-next-line no-redeclare export function FieldValue<T extends FieldType>(field: FieldResult<T>): Opt<T>; +// eslint-disable-next-line no-redeclare export function FieldValue<T extends FieldType>(field: FieldResult<T>, defaultValue?: T): Opt<T> { return field instanceof Promise || field === undefined ? defaultValue : field; } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 1cacfe30c..c73689e1f 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -104,7 +104,7 @@ export const documentSchema = createSchema({ export const collectionSchema = createSchema({ childLayoutTemplate: Doc, // layout template to use to render children of a collecion - childLayoutString: 'string', //layout string to use to render children of a collection + childLayoutString: 'string', // layout string to use to render children of a collection childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template) childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links onChildClick: ScriptField, // script to run for each child when its clicked @@ -113,4 +113,5 @@ export const collectionSchema = createSchema({ }); export type Document = makeInterface<[typeof documentSchema]>; +// eslint-disable-next-line no-redeclare export const Document = makeInterface(documentSchema); diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 4e9d85b6d..4fff2a8e7 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -27,7 +27,6 @@ const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); @observer export class Uploader extends React.Component<ImageUploadProps> { - @observable error: string = ''; @observable nm: string = 'Choose files'; // Text of 'Choose Files' button @observable process: string = ''; // Current status of upload @observable private dialogueBoxOpacity = 1; @@ -40,8 +39,8 @@ export class Uploader extends React.Component<ImageUploadProps> { await Docs.Prototypes.initialize(); const imgPrev = document.getElementById('img_preview'); this.setOpacity(1, '1'); // Slab 1 - if (imgPrev) { - const files: FileList | null = inputRef.current!.files; + if (imgPrev && inputRef.current) { + const { files } = inputRef.current; this.setOpacity(2, '1'); // Slab 2 if (files && files.length !== 0) { this.process = 'Uploading Files'; @@ -69,11 +68,11 @@ export class Uploader extends React.Component<ImageUploadProps> { doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); } this.setOpacity(4, '1'); // Slab 4 - const res = await rp.get(ClientUtils.prepend('/getUserDocumentIds')); - if (!res) { + const docidsRes = await rp.get(ClientUtils.prepend('/getUserDocumentIds')); + if (!docidsRes) { throw new Error('No user id returned'); } - const field = await DocServer.GetRefField(JSON.parse(res).userDocumentId); + const field = await DocServer.GetRefField(JSON.parse(docidsRes).userDocumentId); let pending: Opt<Doc>; if (field instanceof Doc) { pending = col; @@ -100,10 +99,40 @@ export class Uploader extends React.Component<ImageUploadProps> { setTimeout(this.clearUpload, 3000); } } catch (error) { - this.error = JSON.stringify(error); + console.log(JSON.stringify(error)); } }; + // Returns the upload interface for mobile + private get uploadInterface() { + return ( + <div className="imgupload_cont"> + <div className="closeUpload" onClick={() => this.closeUpload()}> + <FontAwesomeIcon icon="window-close" size="lg" /> + </div> + <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: '130' }} /> + <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple /> + <label className="file" id="label" htmlFor="input_image_file"> + {this.nm} + </label> + <div className="upload_label" onClick={this.onClick}> + Upload + </div> + <img id="img_preview" src="" alt="" /> + <div className="loadingImage"> + <div className="loadingSlab" id="slab1" /> + <div className="loadingSlab" id="slab2" /> + <div className="loadingSlab" id="slab3" /> + <div className="loadingSlab" id="slab4" /> + <div className="loadingSlab" id="slab5" /> + <div className="loadingSlab" id="slab6" /> + <div className="loadingSlab" id="slab7" /> + </div> + <p className="status">{this.process}</p> + </div> + ); + } + // Updates label after a files is selected (so user knows a file is uploaded) inputLabel = async () => { const files: FileList | null = await inputRef.current!.files; @@ -112,9 +141,7 @@ export class Uploader extends React.Component<ImageUploadProps> { } else if (files && files.length > 1) { this.nm = files.length.toString() + ' files selected'; } - }; - - // Loops through load icons, and resets buttons + }; // Loops through load icons, and resets buttons @action clearUpload = () => { for (let i = 1; i < 8; i++) { @@ -139,36 +166,6 @@ export class Uploader extends React.Component<ImageUploadProps> { if (slab) slab.style.opacity = opacity; }; - // Returns the upload interface for mobile - private get uploadInterface() { - return ( - <div className="imgupload_cont"> - <div className="closeUpload" onClick={() => this.closeUpload()}> - <FontAwesomeIcon icon="window-close" size="lg" /> - </div> - <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: '130' }} /> - <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== 'Choose files' ? 'active' : ''}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple /> - <label className="file" id="label" htmlFor="input_image_file"> - {this.nm} - </label> - <div className="upload_label" onClick={this.onClick}> - Upload - </div> - <img id="img_preview" src="" alt="" /> - <div className="loadingImage"> - <div className="loadingSlab" id="slab1" /> - <div className="loadingSlab" id="slab2" /> - <div className="loadingSlab" id="slab3" /> - <div className="loadingSlab" id="slab4" /> - <div className="loadingSlab" id="slab5" /> - <div className="loadingSlab" id="slab6" /> - <div className="loadingSlab" id="slab7" /> - </div> - <p className="status">{this.process}</p> - </div> - ); - } - render() { return <MainViewModal contents={this.uploadInterface} isDisplayed interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.closeUpload} />; } diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 1d4c0adce..b355e70a7 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -857,7 +857,7 @@ ScriptingGlobals.add(function switchToMobilePresentation() { return MobileInterface.Instance.setupDefaultPresentation(); }, 'opens the presentation on Dash Mobile'); ScriptingGlobals.add(function openMobileSettings() { - return SettingsManager.Instance.open(); + return SettingsManager.Instance.openMgr(); }, 'opens settings on Dash Mobile'); // Other global functions for mobile diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 1a759f04d..4cb3d8baf 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -231,9 +231,9 @@ export default class UploadManager extends ApiManager { await Promise.all( [...rdocs, ...ldocs].map( doc => - new Promise<void>(res => { + new Promise<void>(dbRes => { // overwrite mongo doc with json doc contents - Database.Instance.replace(doc.id, doc, err => res(err && console.log(err)), true); + Database.Instance.replace(doc.id, doc, err => dbRes(err && console.log(err)), true); }) ) ); |