diff options
| author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-09-26 16:49:43 -0400 |
|---|---|---|
| committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-09-26 16:49:43 -0400 |
| commit | f2958fd4d7dab5369c9e68c5d8f3b50332391aac (patch) | |
| tree | 633e77c196ad4c8c51fb7244e4af22c88168e10d /src/client/views | |
| parent | aa3dc83bdd723db2597dc36fe09ae288ce3641da (diff) | |
| parent | 78edc744c77e378728d033001331737df0b2f393 (diff) | |
Merge branch 'master' into sophie-ai-images
Diffstat (limited to 'src/client/views')
135 files changed, 4673 insertions, 2784 deletions
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index c41ea7053..16e76694d 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -3,6 +3,7 @@ import { observable, action, runInAction } from 'mobx'; import './AntimodeMenu.scss'; import { StrCast } from '../../fields/Types'; import { Doc } from '../../fields/Doc'; +import { SettingsManager } from '../util/SettingsManager'; export interface AntimodeMenuProps {} /** @@ -150,7 +151,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co left: this._left, top: this._top, opacity: this._opacity, - background: StrCast(Doc.UserDoc().userBackgroundColor), + background: SettingsManager.userBackgroundColor, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, @@ -176,7 +177,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co height: 'inherit', width: 200, opacity: this._opacity, - background: StrCast(Doc.UserDoc().userBackgroundColor), + background: SettingsManager.userBackgroundColor, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, @@ -199,7 +200,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co left: this._left, top: this._top, opacity: this._opacity, - background: StrCast(Doc.UserDoc().userBackgroundColor), + background: SettingsManager.userBackgroundColor, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 8412a9aae..adefc7e9c 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -7,6 +7,7 @@ import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextM import { Utils } from '../../Utils'; import { StrCast } from '../../fields/Types'; import { Doc } from '../../fields/Doc'; +import { SettingsManager } from '../util/SettingsManager'; @observer export class ContextMenu extends React.Component { @@ -195,7 +196,7 @@ export class ContextMenu extends React.Component { <div className="contextMenu-group" style={{ - background: StrCast(Doc.UserDoc().userVariantColor), + background: StrCast(SettingsManager.userVariantColor), }}> <div className="contextMenu-description">{value.join(' -> ')}</div> </div> @@ -222,8 +223,8 @@ export class ContextMenu extends React.Component { style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }), - background: StrCast(Doc.UserDoc().userBackgroundColor), - color: StrCast(Doc.UserDoc().userColor), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, }}> {!this.itemsNeedSearch ? null : ( <span className={'search-icon'}> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index b53379435..c2cbca3e1 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -96,7 +96,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select <div className={`contextMenu-item-background`} style={{ - background: SettingsManager.Instance.userColor, + background: SettingsManager.userColor, }} /> </div> @@ -112,7 +112,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select style={{ marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%', marginTop, - background: SettingsManager.Instance.userBackgroundColor, + background: SettingsManager.userBackgroundColor, }}> {this._items.map(prop => ( <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> @@ -146,7 +146,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select <div className={`contextMenu-item-background`} style={{ - background: SettingsManager.Instance.userColor, + background: SettingsManager.userColor, }} /> {submenu} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 231a2d5fb..014a6358f 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, EditableText, FontSize, IconButton, Size, Type } from 'browndash-components'; +import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaPlus } from 'react-icons/fa'; -import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; import { AclPrivate, DocAcl } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; @@ -14,11 +14,11 @@ import { ScriptField } from '../../fields/ScriptField'; import { Cast, ImageCast, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; -import { CollectionViewType } from '../documents/DocumentTypes'; import { HistoryUtil } from '../util/History'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; -import { undoBatch } from '../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionView } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; @@ -36,28 +36,16 @@ enum DashboardGroup { @observer export class DashboardView extends React.Component { - //TODO: delete dashboard, share dashboard, etc. - public static _urlState: HistoryUtil.DocUrl; + @observable private openModal = false; @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; - - @observable private newDashboardName: string | undefined = undefined; - @observable private newDashboardColor: string | undefined = '#AFAFAF'; - @action abortCreateNewDashboard = () => { - this.newDashboardName = undefined; - }; - @action setNewDashboardName = (name: string) => { - this.newDashboardName = name; - }; - @action setNewDashboardColor = (color: string) => { - this.newDashboardColor = color; - }; - - @action - selectDashboardGroup = (group: DashboardGroup) => { - this.selectedDashboardGroup = group; - }; + @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); clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { @@ -69,45 +57,31 @@ export class DashboardView extends React.Component { }; getDashboards = (whichGroup: DashboardGroup) => { - const allDashboards = DocListCast(Doc.MyDashboards.data); if (whichGroup === DashboardGroup.MyDashboards) { - return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); + return DocListCast(Doc.MyDashboards.data).filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); } - const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); - return sharedDashboards; + return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); }; - isUnviewedSharedDashboard = (dashboard: Doc): boolean => { - // const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking); - return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard); - }; - - getSharedDashboards = () => { - const sharedDashs = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking); - return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard)); - }; + isUnviewedSharedDashboard = (dashboard: Doc) => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard); @undoBatch - createNewDashboard = async (name: string, background?: string) => { - setTimeout(() => { - this.abortCreateNewDashboard(); - }, 100); + createNewDashboard = (name: string, background?: string) => { DashboardView.createNewDashboard(undefined, name, background); + this.abortCreateNewDashboard(); }; @computed get namingInterface() { - const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1; - const placeholder = `Dashboard ${dashboardCount}`; return ( <div className="new-dashboard" style={{ - background: StrCast(Doc.UserDoc().userBackgroundColor), - color: StrCast(Doc.UserDoc().userColor), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, }}> <div className="header">Create New Dashboard</div> - <EditableText formLabel="Title" placeholder={placeholder} type={Type.SEC} color={StrCast(Doc.UserDoc().userColor)} setVal={val => this.setNewDashboardName(val as string)} fillWidth /> + <EditableText formLabel="Title" placeholder={this.newDashboardName} type={Type.SEC} color={SettingsManager.userColor} setVal={val => this.setNewDashboardName(val as string)} fillWidth /> <ColorPicker formLabel="Background" // colorPickerType="github" @@ -117,132 +91,118 @@ export class DashboardView extends React.Component { setSelectedColor={this.setNewDashboardColor} /> <div className="button-bar"> - <Button text="Cancel" color={StrCast(Doc.UserDoc().userColor)} onClick={this.abortCreateNewDashboard} /> - <Button type={Type.TERT} text="Create" color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} /> + <Button text="Cancel" color={SettingsManager.userColor} onClick={this.abortCreateNewDashboard} /> + <Button text="Create" color={SettingsManager.userVariantColor} type={Type.TERT} onClick={() => this.createNewDashboard(this.newDashboardName, this.newDashboardColor)} /> </div> </div> ); } + @action + openNewDashboardModal = () => { + this.openModal = true; + this.setNewDashboardName(`Dashboard ${DocListCast(Doc.MyDashboards.data).length + 1}`); + }; _downX: number = 0; _downY: number = 0; - @action - onContextMenu = (dashboard: Doc, e?: React.MouseEvent, pageX?: number, pageY?: number) => { + onContextMenu = (dashboard: Doc, e: React.MouseEvent) => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 - if (e) { + if (navigator.userAgent.includes('Mozilla') || (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3)) { e.preventDefault(); e.stopPropagation(); - e.persist(); - if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { - return; - } - const cm = ContextMenu.Instance; - cm.addItem({ - description: 'Share Dashboard', - event: async () => { - SharingManager.Instance.open(undefined, dashboard); - }, + ContextMenu.Instance.addItem({ + description: `Share Dashboard`, + event: () => SharingManager.Instance.open(undefined, dashboard), icon: 'edit', }); - cm.addItem({ - description: 'Delete Dashboard', - event: async () => { - DashboardView.removeDashboard(dashboard); - }, + ContextMenu.Instance.addItem({ + description: `Delete Dashboard ${Doc.noviceMode ? '(disabled)' : ''}`, + event: () => !Doc.noviceMode && DashboardView.removeDashboard(dashboard), icon: 'trash', }); - cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } }; render() { - const color = StrCast(Doc.UserDoc().userColor); - const variant = StrCast(Doc.UserDoc().userVariantColor); + const color = SettingsManager.userColor; + const variant = SettingsManager.userVariantColor; return ( <> <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 + ')'} active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards} color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color} - align={'flex-start'} + align="flex-start" onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)} fillWidth /> - <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={() => this.setNewDashboardName('')} /> + <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={this.openNewDashboardModal} /> </div> <div className="all-dashboards"> - {this.getDashboards(this.selectedDashboardGroup).map(dashboard => { - const href = ImageCast(dashboard.thumb)?.url?.href; - const shared = Object.keys(dashboard[DocAcl]) - .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key)) - .some(key => dashboard[DocAcl][key] !== AclPrivate); - return ( + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && !this.getDashboards(this.selectedDashboardGroup).length + ? 'No one has shared a dashboard with you' + : this.getDashboards(this.selectedDashboardGroup).map(dashboard => { + const href = ImageCast(dashboard.thumb)?.url?.href; + const shared = Object.keys(dashboard[DocAcl]) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key)) + .some(key => dashboard[DocAcl][key] !== AclPrivate); + return ( + <div + className="dashboard-container" + key={dashboard[Id]} + style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }} + onContextMenu={e => this.onContextMenu(dashboard, e)} + onClick={e => this.clickDashboard(e, dashboard)}> + <img + 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 => (Doc.GetProto(dashboard).title = val)} /> + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} + <div + className="more" + onPointerDown={e => { + this._downX = e.clientX; + this._downY = e.clientY; + }} + onClick={e => this.onContextMenu(dashboard, e)}> + <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} /> + </div> + </div> + <div + className="background" + style={{ + background: SettingsManager.userColor, + filter: 'opacity(0.2)', + }} + /> + <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> + </div> + ); + })} + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? null : ( + <div className="dashboard-container-new" onClick={this.openNewDashboardModal}> + + <div - className="dashboard-container" - key={dashboard[Id]} - style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }} - onContextMenu={e => this.onContextMenu(dashboard, e)} - onClick={e => this.clickDashboard(e, dashboard)}> - <img - 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 => (Doc.GetProto(dashboard).title = val)} /> - {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} - <div - className="more" - onPointerDown={e => { - this._downX = e.clientX; - this._downY = e.clientY; - }} - onClick={e => { - e.preventDefault(); - e.stopPropagation(); - this.onContextMenu(dashboard, e); - }}> - <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} /> - </div> - </div> - <div - className={`background`} - style={{ - background: StrCast(Doc.UserDoc().userColor), - filter: 'opacity(0.2)', - }} - /> - <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> - </div> - ); - })} - <div - className="dashboard-container-new" - onClick={() => { - this.setNewDashboardName(''); - }}> - + - <div - className={`background`} - style={{ - background: StrCast(Doc.UserDoc().userColor), - filter: 'opacity(0.2)', - }} - /> - </div> + className="background" + style={{ + background: SettingsManager.userColor, + filter: 'opacity(0.2)', + }} + /> + </div> + )} </div> </div> - <MainViewModal - contents={this.namingInterface} - isDisplayed={this.newDashboardName !== undefined} - interactive={true} - closeOnExternalClick={this.abortCreateNewDashboard} - dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} - /> + <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive={true} closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} /> </> ); } @@ -264,6 +224,7 @@ export class DashboardView extends React.Component { public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { if (!doc) return false; Doc.AddDocToList(Doc.MyDashboards, 'data', doc); + Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', doc); // this has the side-effect of setting the main container since we're assigning the active/guest dashboard Doc.UserDoc() ? (Doc.ActiveDashboard = doc) : (Doc.GuestDashboard = doc); @@ -298,13 +259,14 @@ export class DashboardView extends React.Component { return true; }; - public static removeDashboard = async (dashboard: Doc) => { - const dashboards = await DocListCastAsync(Doc.MyDashboards.data); - if (dashboards?.length) { - if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.find(doc => doc !== dashboard)); + public static removeDashboard = (dashboard: Doc) => { + const dashboards = DocListCast(Doc.MyDashboards.data).filter(dash => dash !== dashboard); + undoable(() => { + if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.lastElement()); Doc.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard); + Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashboard, undefined, true, true); if (!dashboards.length) Doc.ActivePage = 'home'; - } + }, 'remove dashboard')(); }; public static resetDashboard = (dashboard: Doc) => { @@ -402,11 +364,14 @@ export class DashboardView extends React.Component { backgroundColor: background, title: `Untitled Tab 1`, }; + const title = name ? name : `Dashboard ${dashboardCount}`; const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); + Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc); dashboardDoc['pane-count'] = 1; + freeformDoc.embedContainer = dashboardDoc; Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); @@ -486,6 +451,8 @@ ScriptingGlobals.add(function resetDashboard(dashboard: Doc) { ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { DashboardView.openDashboard(Doc.MakeEmbedding(dashboard)); }, 'adds Dashboard to set of Dashboards'); -ScriptingGlobals.add(function snapshotDashboard() { - DashboardView.snapshotDashboard(); +ScriptingGlobals.add(async function snapshotDashboard() { + const batch = UndoManager.StartBatch('snapshot'); + await DashboardView.snapshotDashboard(); + batch.end(); }, 'creates a snapshot copy of a dashboard'); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index e076e69ca..483b92957 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -139,7 +139,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() const indocs = doc instanceof Doc ? [doc] : doc; const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin); - docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true)); + // docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true)); const targetDataDoc = this.dataDoc; const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); @@ -194,6 +194,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() added.forEach(doc => { doc._dragOnlyWithinContainer = undefined; if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + else Doc.GetProto(doc).annotationOn = undefined; Doc.SetContainer(doc, this.rootDoc); inheritParentAcls(targetDataDoc, doc, true); }); @@ -201,7 +202,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add))); else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); - targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(new Date(Date.now())); + targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(); } } return true; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 345135a1a..c1ec5b4a4 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -562,7 +562,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const rootView = this.props.views()[0]; const rootDoc = rootView?.rootDoc; if (rootDoc) { - const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc; + const anchor = rootView.ComponentView?.getAnchor?.(true) ?? rootDoc; const trail = DocCast(anchor.presentationTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true); if (trail !== anchor.presentationTrail) { DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' }); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f3daf3ffa..40eb1fe2b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -57,7 +57,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @observable private _editingTitle = false; @observable private _hidden = false; @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. - @observable public Interacting = false; @observable public pushIcon: IconProp = 'arrow-alt-circle-up'; @observable public pullIcon: IconProp = 'arrow-alt-circle-down'; @observable public pullColor: string = 'white'; @@ -80,8 +79,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // show decorations whenever pointer moves outside of selection bounds. 'pointermove', action(e => { - if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { - this._showNothing = false; + if (this.Bounds.x || this.Bounds.y || this.Bounds.r || this.Bounds.b) { + if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { + this._showNothing = false; + } } }) ); @@ -89,7 +90,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @computed get Bounds() { - if (LinkFollower.IsFollowing) return { x: 0, y: 0, r: 0, b: 0 }; + if (LinkFollower.IsFollowing || DocumentView.ExploreMode) return { x: 0, y: 0, r: 0, b: 0 }; const views = SelectionManager.Views(); return views .filter(dv => dv.props.renderDepth > 0) @@ -261,6 +262,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } }); if (!this._iconifyBatch) { + (document.activeElement as any).blur?.(); this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying'); } else { forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes @@ -328,7 +330,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P */ @action onRadiusDown = (e: React.PointerEvent): void => { - this._isRounding = DocumentDecorations.Instance.Interacting = true; + this._isRounding = DocumentView.Interacting = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); // Call util move event function setupMoveUpEvents( @@ -351,7 +353,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return false; }, // moveEvent action(e => { - DocumentDecorations.Instance.Interacting = this._isRounding = false; + DocumentView.Interacting = this._isRounding = false; this._resizeUndo?.end(); }), // upEvent e => {}, // clickEvent, @@ -476,7 +478,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); - this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + DocumentView.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeHdlId = e.currentTarget.className; const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX; @@ -686,7 +688,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerUp = (e: PointerEvent): void => { this._resizeHdlId = ''; - this.Interacting = false; + DocumentView.Interacting = false; this._resizeUndo?.end(); SnappingManager.clearSnapLines(); @@ -803,8 +805,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P </Tooltip> ); - const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme); - const leftBounds = this.props.boundsLeft; const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; @@ -817,7 +817,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate? const rotation = SelectionManager.Views().length == 1 ? NumCast(seldocview.rootDoc._rotation) : 0; - const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; + const resizerScheme = ''; // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; @@ -827,13 +827,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); - const sharingMenu = docShareMode ? ( - <div className="documentDecorations-share"> - <div className={`documentDecorations-share${shareMode}`}> - - {shareSymbolIcon + ' ' + shareMode} - - {/* {!Doc.noviceMode ? ( + const sharingMenu = + Doc.IsSharingEnabled && docShareMode ? ( + <div className="documentDecorations-share"> + <div className={`documentDecorations-share${shareMode}`}> + + {shareSymbolIcon + ' ' + shareMode} + + {/* {!Doc.noviceMode ? ( <div className="checkbox"> <div className="checkbox-box"> <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} /> @@ -842,16 +843,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P </div> ) : null} */} + </div> </div> - </div> - ) : ( - <div /> - ); + ) : ( + <div /> + ); const titleArea = this._editingTitle ? ( <input ref={this._keyinput} - className={`documentDecorations-title${colorScheme}`} + className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" @@ -869,7 +870,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P e.stopPropagation; }}> {hideTitle ? null : ( - <span className={`documentDecorations-titleSpan${colorScheme}`} onPointerDown={this.onTitleDown}> + <span className="documentDecorations-titleSpan" onPointerDown={this.onTitleDown}> {this.selectionTitle} </span> )} @@ -885,7 +886,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); return ( - <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> + <div className="documentDecorations" style={{ display: this._showNothing ? 'none' : undefined }}> <div className="documentDecorations-background" style={{ @@ -893,7 +894,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P height: bounds.b - bounds.y + this._resizeBorderWidth + 'px', left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? 'none' : 'all', + pointerEvents: DocumentDecorations.Instance.AddToSelection || DocumentView.Interacting ? 'none' : 'all', display: SelectionManager.Views().length <= 1 || hideDecorations ? 'none' : undefined, }} onPointerDown={this.onBackgroundDown} diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index 0955ba8ff..f7c03caf9 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -3,7 +3,8 @@ overflow-wrap: break-word; word-wrap: break-word; hyphens: auto; - overflow: hidden; + overflow: auto; + height: 100%; min-width: 20; text-overflow: ellipsis; } @@ -11,6 +12,7 @@ .editableView-container-editing-oneLine { width: 100%; height: max-content; + overflow: hidden; span { p { diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index d60617020..ca4ffaf3a 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -6,7 +6,6 @@ import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; -import { EditableText } from 'browndash-components'; export interface EditableProps { /** @@ -232,7 +231,7 @@ export class EditableView extends React.Component<EditableProps> { onChange: this.props.autosuggestProps.onChange, }} /> - ) : ( + ) : this.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? ( <input className="editableView-input" ref={r => (this._inputref = r)} @@ -248,31 +247,31 @@ export class EditableView extends React.Component<EditableProps> { onClick={this.stopPropagation} onPointerUp={this.stopPropagation} /> + ) : ( + <textarea + className="editableView-input" + ref={r => (this._inputref = r)} + 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} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyPress={this.stopPropagation} + onPointerDown={this.stopPropagation} + onClick={this.stopPropagation} + onPointerUp={this.stopPropagation} + /> ); - // ) : ( - // <textarea - // className="editableView-input" - // ref={r => (this._inputref = r)} - // 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} - // onChange={this.onChange} - // onKeyDown={this.onKeyDown} - // onKeyPress={this.stopPropagation} - // onPointerDown={this.stopPropagation} - // onClick={this.stopPropagation} - // onPointerUp={this.stopPropagation} - // /> - // ); } render() { - if (this._editing && this.props.GetValue() !== undefined) { + const gval = this.props.GetValue()?.replace(/\n/g, '\\r\\n'); + if (this._editing && gval !== undefined) { return this.props.sizeToContent ? ( <div style={{ display: 'grid', minWidth: 100 }}> - <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.props.GetValue()}</div> + <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{gval}</div> {this.renderEditor()} </div> ) : ( diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss index 4f0460659..d6d2956aa 100644 --- a/src/client/views/FilterPanel.scss +++ b/src/client/views/FilterPanel.scss @@ -135,7 +135,7 @@ .filterBox-addFilter { width: 120px; - background-color: #e9e9e9; + backgroundcolor: #e9e9e9; border-radius: 12px; padding: 5px; margin: 5px; @@ -156,7 +156,7 @@ right: 0; top: 0; z-index: 1; - // background-color: #9f9f9f; + // background-color: #9f9f9f; .filterBox-tree { z-index: 0; @@ -191,22 +191,60 @@ } } +.filterBox-facetHeader { + display: flex; + align-items: center; + // float:right; + .filterBox-facetHeader-collapse { + // float: right; + // justify-items: right; + // align-items: flex-end; + margin-left: auto; + // margin-right: 9px; -.filterBox-facetHeader{ - display: flex; - align-items: center; - // float:right; - - .filterBox-facetHeader-collapse{ float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; + font-size: 16; } + .filterBox-facetHeader-remove { + // margin-left: auto; + float: right; + font-size: 16; + font-weight: bold; + } } - - +.filterbox-collpasedAndActive { + // left:100px; + text-indent: 18px; + // background-color: pink; + font-size: 12px; + font-weight: bold; +} + +.sliderBox-outerDiv { + display: inline-block; + vertical-align: middle; +} + +// .sliderBox-outerDiv { +// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box +// height: 40; // height: 100%; +// border-radius: inherit; +// display: flex; +// flex-direction: column; +// position: relative; +// // background-color: yellow; +// .slider-tracks { +// top: 7px; +// position: relative; +// } +// .slider-ticks { +// position: relative; +// } +// .slider-handles { +// top: 7px; +// position: relative; +// } +// } diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 63bd01b19..69ceb0f65 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -1,24 +1,32 @@ import React = require('react'); import { action, computed, observable, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; +import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; +import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai'; +import { CiCircleRemove } from 'react-icons/ci'; import Select from 'react-select'; import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; -import { StrCast } from '../../fields/Types'; +import { DocumentOptions, FInfo } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { UserOptions } from '../util/GroupManager'; +import { SearchUtil } from '../util/SearchUtil'; +import { undoable } from '../util/UndoManager'; import './FilterPanel.scss'; import { FieldView } from './nodes/FieldView'; -import { SearchBox } from './search/SearchBox'; -import { undoable } from '../util/UndoManager'; -import { AiOutlineMinusSquare } from 'react-icons/ai'; -import { CiCircleRemove } from 'react-icons/ci'; +import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components'; +import { SettingsManager } from '../util/SettingsManager'; +import { Id } from '../../fields/FieldSymbols'; +import { List } from '../../fields/List'; interface filterProps { rootDoc: Doc; } + @observer export class FilterPanel extends React.Component<filterProps> { + private _documentOptions: DocumentOptions = new DocumentOptions(); + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterPanel, fieldKey); } @@ -41,43 +49,77 @@ export class FilterPanel extends React.Component<filterProps> { const allDocs = new Set<Doc>(); const targetDoc = this.targetDoc; if (targetDoc) { - SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); + SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); } + console.log('this is all Docs' + Array.from(allDocs)); return Array.from(allDocs); } @computed get _allFacets() { // trace(); - const noviceReqFields = ['author', 'tags', 'text', 'type']; + const noviceReqFields = ['author', 'tags', 'text', 'type', '-linkedTo']; const noviceLayoutFields: string[] = []; //["_layout_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set<string>(noviceFields); - this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); + this.allDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key))); const sortedKeys = Array.from(keys.keys()) .filter(key => key[0]) .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) .sort(); + + // console.log('THIS IS HERE ' + Doc.UserDoc().color + 'space ' + Doc.UserDoc().color); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); + + console.log('this is novice fields ' + noviceFields + 'and this is sorted Keys ' + sortedKeys); + return [...noviceFields, ...sortedKeys]; } + @computed get rangeFilters() { + return StrListCast(this.targetDoc?._childFiltersByRanges).filter((filter, i) => !(i % 3)); + } + /** - * The current attributes selected to filter based on + * activeFilters( ) -- all filters that currently have a filter set on them in this document (ranges, and others) + * ["#tags::bob::check", "tags::joe::check", "width", "height"] */ @computed get activeFilters() { - return StrListCast(this.targetDoc?._childFilters); + return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters); } + @computed get mapActiveFiltersToFacets() { + const filters = new Map<string, string>(); + //this.targetDoc.docFilters + this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1], filter.split(Doc.FilterSep)[0])); + return filters; + } + + // + // activeFacetHeaders() - just the facet names, not the rest of the filter + // + // this wants to return all the filter facets that have an existing filter set on them in order to show them in the rendered panel + // this set may overlap the selectedFilters + // if the components reloads, these will still exist and be shown + + // ["#tags", "width", "height"] + // + + @computed get activeFacetHeaders() { + const activeHeaders = new Array(); + this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0])); + + return activeHeaders; + } /** * @returns a string array of the current attributes */ - @computed get currentFacets() { - return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); - } + // @computed get currentFacets() { + // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); + // } gatherFieldValues(childDocs: Doc[], facetKey: string) { - const valueSet = new Set<string>(); + const valueSet = new Set<string>(StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1])); let rtFields = 0; let subDocs = childDocs; if (subDocs.length > 0) { @@ -99,6 +141,7 @@ export class FilterPanel extends React.Component<filterProps> { } // } // }); + return { strings: Array.from(valueSet.keys()), rtFields }; } @@ -107,46 +150,93 @@ export class FilterPanel extends React.Component<filterProps> { Doc.setDocRangeFilter(this.targetDoc, filterName, undefined); }; - @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); - @computed get activeFacets() { - const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets); - StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')); - setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')))); - return facets; + // @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); + @observable _chosenFacetsCollapse = new ObservableMap<string, boolean>(); + @observable _collapseReturnKeys = new Array(); + + // this computed function gets the active filters and maps them to their headers + + // + // activeRenderedFacetInfos() + // returns renderInfo for all user selected filters and for all existing filters set on the document + // Map("tags" => {"checkbox"}, + // "width" => {"rangs", domain:[1978,1992]}) + // + + @computed get activeRenderedFacetInfos() { + return new Set( + Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => { + const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, + maxVal = -Number.MAX_VALUE; + facetValues.strings.map(val => { + const num = val ? Number(val) : Number.NaN; + if (Number.isNaN(num)) { + val && nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + + if (facetHeader === 'text') { + return { facetHeader, renderType: 'text' }; + } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { + const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); + const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); + const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter + return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] }; + } else { + return { facetHeader, renderType: 'checkbox' }; + } + }) + ); } + + @observable _selectedFacetHeaders = new Set<string>(); + /** - * Responds to clicking the check box in the flyout menu + * user clicks on a filter facet because they want to see it. + * this adds this chosen filter to a set of user selected filters called: selectedFilters + * if this component reloads, then these filters will go away since they haven't been written to any Doc anywhere + * + * // this._selectedFacets.add(facetHeader); .. add to Set() not array */ + @action facetClick = (facetHeader: string) => { - if (!this.targetDoc) return; - const allCollectionDocs = this.targetDocChildren; - const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + // just when someone chooses a facet - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, - maxVal = -Number.MAX_VALUE; - facetValues.strings.map(val => { - const num = val ? Number(val) : Number.NaN; - if (Number.isNaN(num)) { - val && nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); + this._selectedFacetHeaders.add(facetHeader); + + return; + }; + + @action + sortingCurrentFacetValues = (facetHeader: string) => { + this._collapseReturnKeys.splice(0); + + Array.from(this.activeRenderedFacetInfos.keys()).map(renderInfo => { + if (renderInfo.renderType === 'range' && renderInfo.facetHeader === facetHeader && renderInfo.range) { + this._collapseReturnKeys.push(renderInfo.range.map(number => number.toFixed(2))); } }); - if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) { - this._chosenFacets.set(facetHeader, 'text'); - } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { - } else { - this._chosenFacets.set(facetHeader, 'checkbox'); + + for (var key of this.facetValues(facetHeader)) { + if (this.mapActiveFiltersToFacets.get(key)) { + this._collapseReturnKeys.push(key); + } } + + return <div className=" filterbox-collpasedAndActive">{this._collapseReturnKeys.join(', ')}</div>; }; facetValues = (facetHeader: string) => { const allCollectionDocs = new Set<Doc>(); - SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); - const set = new Set<string>([String.fromCharCode(127) + '--undefined--']); + SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); + const set = new Set<string>([...StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]); if (facetHeader === 'tags') allCollectionDocs.forEach(child => StrListCast(child[facetHeader]) @@ -156,8 +246,11 @@ export class FilterPanel extends React.Component<filterProps> { else allCollectionDocs.forEach(child => { const fieldVal = child[facetHeader] as Field; - set.add(Field.toString(fieldVal)); - (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); + if (!(fieldVal instanceof List)) { + // currently we have no good way of filtering based on a field that is a list + set.add(Field.toString(fieldVal)); + (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); + } }); const facetValues = Array.from(set).filter(v => v); @@ -168,12 +261,55 @@ export class FilterPanel extends React.Component<filterProps> { }; render() { - const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); + let filteredOptions: string[] = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; + + Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => { + if (pair[1].filterable) { + filteredOptions.push(pair[0]); + } + }); + + let options = filteredOptions.map(facet => ({ value: facet, label: facet })); + return ( <div className="filterBox-treeView"> <div className="filterBox-select"> <div style={{ width: '100%' }}> - <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} /> + <Select + styles={{ + control: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + placeholder: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + input: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + option: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, + }), + menuList: (baseStyles, state) => ({ + ...baseStyles, + backgroundColor: SettingsManager.userBackgroundColor, + }), + }} + placeholder="Add a filter..." + options={options} + isMulti={false} + onChange={val => this.facetClick((val as UserOptions).value)} + onKeyDown={e => e.stopPropagation()} + value={null} + closeMenuOnSelect={true} + /> </div> {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} {/* <div className="filterBox-select-bool"> @@ -188,36 +324,72 @@ export class FilterPanel extends React.Component<filterProps> { </div> <div className="filterBox-tree" key="tree"> - {Array.from(this.activeFacets.keys()).map(facetHeader => ( - <div> - <div className="filterBox-facetHeader"> - <div className="filterBox-facetHeader-Header"> </div> - {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)} + {Array.from(this.activeRenderedFacetInfos.keys()).map( + ( + renderInfo // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader + ) => ( + <div> + <div className="filterBox-facetHeader"> + <div className="filterBox-facetHeader-Header"> </div> + {renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)} + + <div + className="filterBox-facetHeader-collapse" + onClick={action(e => { + const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader); + this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue); + })}> + {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? <AiOutlinePlusSquare /> : <AiOutlineMinusSquare />} + </div> - <div className="filterBox-facetHeader-collapse"> - <AiOutlineMinusSquare /> - {/* <CiCircleRemove/> */} + <div + className="filterBox-facetHeader-remove" + onClick={action(e => { + if (renderInfo.facetHeader === 'text') { + Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, 'match', 'remove'); + } else { + for (var key of this.facetValues(renderInfo.facetHeader)) { + if (this.mapActiveFiltersToFacets.get(key)) { + Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, key, 'remove'); + } + } + } + this._selectedFacetHeaders.delete(renderInfo.facetHeader); + this._chosenFacetsCollapse.delete(renderInfo.facetHeader); + + if (renderInfo.domain) { + Doc.setDocRangeFilter(this.targetDoc, renderInfo.facetHeader, renderInfo.domain, 'remove'); + } + })}> + <CiCircleRemove />{' '} + </div> </div> - </div> - {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)} - </div> - ))} + {this._chosenFacetsCollapse.get(renderInfo.facetHeader) + ? this.sortingCurrentFacetValues(renderInfo.facetHeader) + : this.displayFacetValueFilterUIs(renderInfo.renderType, renderInfo.facetHeader, renderInfo.domain, renderInfo.range)} + {/* */} + </div> + ) + )} </div> </div> ); } - private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode { + private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string, renderInfoDomain?: number[] | undefined, renderInfoRange?: number[]): React.ReactNode { switch (type) { case 'text': return ( <input - placeholder={ + key={this.targetDoc[Id]} + placeholder={'enter text to match'} + defaultValue={ StrListCast(this.targetDoc._childFilters) .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader) - ?.split(Doc.FilterSep)[1] ?? '-empty-' + ?.split(Doc.FilterSep)[1] } + style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} /> @@ -229,11 +401,11 @@ export class FilterPanel extends React.Component<filterProps> { <div> <input style={{ width: 20, marginLeft: 20 }} - checked={ + checked={['check', 'exists'].includes( StrListCast(this.targetDoc._childFilters) .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue) - ?.split(Doc.FilterSep)[2] === 'check' - } + ?.split(Doc.FilterSep)[2] ?? '' + )} type={type} onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> @@ -241,6 +413,82 @@ export class FilterPanel extends React.Component<filterProps> { </div> ); }); + + case 'range': + const domain = renderInfoDomain; + const range = renderInfoRange; + + if (range) { + console.log('this is info range ' + range[0] + ' , ' + range[1]); + } + + if (domain) { + console.log('this is info domain ' + domain[0] + ', ' + domain[1]); + + return ( + <> + {/* <div className="sliderBox-outerDiv-checkBox" style={{ float: 'left' }}> + <Checkbox color="primary" onChange={action(() => console.log('on change'))} /> + </div> */} + + <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}> + <Slider + mode={2} + step={Math.min(1, 0.1 * (domain[1] - domain[0]))} + domain={[domain[0], domain[1]]} // -1000, 1000 + rootStyle={{ position: 'relative', width: '100%' }} + onChange={values => Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)} + values={renderInfoRange!}> + <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> + <Handles> + {({ handles, activeHandleID, getHandleProps }) => ( + <div className="slider-handles"> + {handles.map((handle, i) => { + // const value = i === 0 ? defaultValues[0] : defaultValues[1]; + return ( + <div> + <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> + </div> + ); + })} + </div> + )} + </Handles> + <Tracks left={false} right={false}> + {({ tracks, getTrackProps }) => ( + <div className="slider-tracks"> + {tracks.map(({ id, source, target }) => ( + <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> + ))} + </div> + )} + </Tracks> + <Ticks count={5}> + {({ ticks }) => ( + <div className="slider-ticks"> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> + ))} + </div> + )} + </Ticks> + </Slider> + </div> + </> + ); + } + + // case 'range' + // return <Slider ... + // return <slider domain={renderInfo.domain}> domain is number[] for min and max + // onChange = { ... Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) } + // + // OR + + // return <div> + // <slider domain={renderInfo.domain}> // domain is number[] for min and max + // <dimain changing handles > + // <?div } } } diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx index 71e0ff63c..65f6a6dfa 100644 --- a/src/client/views/InkTangentHandles.tsx +++ b/src/client/views/InkTangentHandles.tsx @@ -12,7 +12,6 @@ import { UndoManager } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; -import { DocumentView } from './nodes/DocumentView'; export interface InkHandlesProps { inkDoc: Doc; inkView: InkingStroke; diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 6c213f40f..258bfad66 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -260,7 +260,10 @@ export class InkTranscription extends React.Component { CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those const selected = ffView.unprocessedDocs; - const newCollection = this.groupInkDocs(selected, ffView); + const newCollection = this.groupInkDocs( + selected.filter(doc => doc.embedContainer), + ffView + ); ffView.unprocessedDocs = []; InkTranscription.Instance.transcribeInk(newCollection, selected, false); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index b3647249a..f63a27426 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -82,9 +82,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1); getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - if (this._subContentView) { - return this._subContentView.getAnchor?.(addAsAnnotation) || this.rootDoc; - } + const subAnchor = this._subContentView?.getAnchor?.(addAsAnnotation); + if (subAnchor !== this.rootDoc && subAnchor) return subAnchor; if (!addAsAnnotation && !pinProps) return this.rootDoc; @@ -389,6 +388,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[Height]())); }); } + const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); + const highlightIndex = highlight?.highlightIndex; + const highlightColor = (!this.props.isSelected() || !isInkMask) && highlight?.highlightIndex ? highlight?.highlightColor : undefined; + const color = StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); // Visually renders the polygonal line made by the user. const inkLine = InteractionUtils.CreatePolyline( @@ -411,31 +414,28 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { '', 'none', 1.0, - false + false, + undefined, + undefined, + color === 'transparent' ? highlightColor : undefined ); - const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); - const highlightIndex = highlight?.highlightIndex; - const highlightColor = - (!this.props.isSelected() || !isInkMask) && highlight?.highlightIndex - ? highlight?.highlightColor - : StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); // Invisible polygonal line that enables the ink to be selected by the user. const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) => InteractionUtils.CreatePolyline( inkData, inkLeft, inkTop, - mask && highlightColor === 'transparent' ? this.strokeColor : highlightColor, + mask && color === 'transparent' ? this.strokeColor : color, inkStrokeWidth, - inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), + inkStrokeWidth + NumCast(this.layoutDoc.stroke_borderWidth) + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), StrCast(this.layoutDoc.stroke_lineJoin), StrCast(this.layoutDoc.stroke_lineCap), StrCast(this.layoutDoc.stroke_bezier), !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor, - '', - '', + startMarker, + endMarker, markerScale, - undefined, + StrCast(this.layoutDoc.stroke_dash), inkScaleX, inkScaleY, '', @@ -443,7 +443,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { 0.0, false, downHdlr, - mask + mask, + highlightColor ); const fsize = +StrCast(this.props.Document._text_fontSize, '12px').replace('px', ''); // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size. diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss index f86a1d211..9a9b8a437 100644 --- a/src/client/views/LightboxView.scss +++ b/src/client/views/LightboxView.scss @@ -5,7 +5,6 @@ top: 10; background: transparent; border-radius: 8; - color: white; opacity: 0.7; width: 25; flex-direction: column; @@ -17,11 +16,10 @@ .lightboxView-tabBtn { margin: auto; position: absolute; - right: 38; + right: 45; top: 10; background: transparent; border-radius: 8; - color: white; opacity: 0.7; width: 25; flex-direction: column; @@ -33,11 +31,10 @@ .lightboxView-penBtn { margin: auto; position: absolute; - right: 70; + right: 80; top: 10; background: transparent; border-radius: 8; - color: white; opacity: 0.7; width: 25; flex-direction: column; @@ -49,11 +46,10 @@ .lightboxView-exploreBtn { margin: auto; position: absolute; - right: 100; + right: 115; top: 10; background: transparent; border-radius: 8; - color: white; opacity: 0.7; width: 25; flex-direction: column; @@ -68,7 +64,6 @@ left: 0; width: 100%; height: 100%; - background: #000000bb; z-index: 1000; .lightboxView-contents { position: absolute; diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index afb76b9ac..93eaec959 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -1,22 +1,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { InkTool } from '../../fields/InkField'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline'; import { TabDocView } from './collections/TabDocView'; import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; -import { MainView } from './MainView'; import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; @@ -61,7 +62,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._childFilters && (this._childFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; - MainView.Instance._exploreMode = false; + DocumentView.ExploreMode = false; } else { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); @@ -119,7 +120,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), bottom, }}> - <div className="lightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}> + <div className="lightboxView-navBtn" title={color} style={{ top, color: SettingsManager.userColor, background: undefined }} onClick={click}> <div style={{ height: 10 }}>{color}</div> <FontAwesomeIcon icon={icon as any} size="3x" /> </div> @@ -230,6 +231,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { downx = e.clientX; downy = e.clientY; }} + style={{ background: SettingsManager.userBackgroundColor }} onClick={e => { if (Math.abs(downx - e.clientX) < 4 && Math.abs(downy - e.clientY) < 4) { LightboxView.SetLightboxDoc(undefined); @@ -243,6 +245,8 @@ export class LightboxView extends React.Component<LightboxViewProps> { width: this.lightboxWidth(), height: this.lightboxHeight(), clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`, + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, }}> {/* <CollectionMenu /> TODO:glr This is where it would go*/} @@ -270,7 +274,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { addDocTab={this.addDocTab} pinToPres={TabDocView.PinDoc} bringToFront={emptyFunction} - onBrowseClick={MainView.Instance.exploreMode} + onBrowseClick={DocumentView.exploreMode} focus={emptyFunction} /> </GestureOverlay> @@ -300,47 +304,68 @@ export class LightboxView extends React.Component<LightboxViewProps> { this.future()?.length.toString() )} <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} /> - <div - className="lightboxView-navBtn" - title="toggle fit width" - onClick={e => { - e.stopPropagation(); - LightboxView.LightboxDoc!._layout_fitWidth = !LightboxView.LightboxDoc!._layout_fitWidth; - }}> - <FontAwesomeIcon icon={LightboxView.LightboxDoc?._layout_fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" /> + <div className="lightboxView-navBtn"> + <Toggle + tooltip="toggle reading view" + color={SettingsManager.userColor} + background={BoolCast(LightboxView.LightboxDoc!._layout_fitWidth) ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + toggleType={ToggleType.BUTTON} + type={Type.TERT} + toggleStatus={BoolCast(LightboxView.LightboxDoc!._layout_fitWidth)} + onClick={e => { + e.stopPropagation(); + LightboxView.LightboxDoc!._layout_fitWidth = !LightboxView.LightboxDoc!._layout_fitWidth; + }} + icon={<FontAwesomeIcon icon={LightboxView.LightboxDoc?._layout_fitWidth ? 'book-open' : 'book'} size="sm" />} + /> </div> - <div - className="lightboxView-tabBtn" - title="open in tab" - onClick={e => { - const lightdoc = LightboxView._docTarget || LightboxView._doc!; - e.stopPropagation(); - Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightdoc); - CollectionDockingView.AddSplit(lightdoc, OpenWhereMod.none); - SelectionManager.DeselectAll(); - LightboxView.SetLightboxDoc(undefined); - }}> - <FontAwesomeIcon icon={'file-download'} size="2x" /> + <div className="lightboxView-tabBtn"> + <Toggle + tooltip="open document in a tab" + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + toggleType={ToggleType.BUTTON} + type={Type.TERT} + icon={<FontAwesomeIcon icon="file-download" size="sm" />} + onClick={e => { + const lightdoc = LightboxView._docTarget || LightboxView._doc!; + e.stopPropagation(); + Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightdoc); + CollectionDockingView.AddSplit(lightdoc, OpenWhereMod.none); + SelectionManager.DeselectAll(); + LightboxView.SetLightboxDoc(undefined); + }} + /> </div> - <div - className="lightboxView-penBtn" - title="toggle pen annotation" - style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }} - onClick={e => { - e.stopPropagation(); - Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; - }}> - <FontAwesomeIcon color={Doc.ActiveTool === InkTool.Pen ? 'black' : 'white'} icon={'pen'} size="2x" /> + <div className="lightboxView-penBtn"> + <Toggle + tooltip="toggle pen annotation" + color={SettingsManager.userColor} + background={Doc.ActiveTool === InkTool.Pen ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + toggleType={ToggleType.BUTTON} + toggleStatus={Doc.ActiveTool === InkTool.Pen} + type={Type.TERT} + icon={<FontAwesomeIcon icon="pen" size="sm" />} + onClick={e => { + e.stopPropagation(); + Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; + }} + /> </div> - <div - className="lightboxView-exploreBtn" - title="toggle explore mode to navigate among documents only" - style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }} - onClick={action(e => { - e.stopPropagation(); - MainView.Instance._exploreMode = !MainView.Instance._exploreMode; - })}> - <FontAwesomeIcon color={MainView.Instance._exploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" /> + <div className="lightboxView-exploreBtn"> + <Toggle + tooltip="toggle explore mode to navigate among documents only" + color={SettingsManager.userColor} + background={DocumentView.ExploreMode ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + toggleType={ToggleType.BUTTON} + type={Type.TERT} + toggleStatus={DocumentView.ExploreMode} + icon={<FontAwesomeIcon icon="globe-americas" size="sm" />} + onClick={action(e => { + e.stopPropagation(); + DocumentView.ExploreMode = !DocumentView.ExploreMode; + })} + /> </div> </div> ); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6dd1d53ee..96bd52d39 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -2,18 +2,19 @@ // (module as any).hot.accept(); // } +import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { AssignAllExtensions } from '../../extensions/General/Extensions'; import { FieldLoader } from '../../fields/FieldLoader'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { PingManager } from '../util/PingManager'; import { ReplayMovements } from '../util/ReplayMovements'; import { TrackMovements } from '../util/TrackMovements'; import { CollectionView } from './collections/CollectionView'; -import { MainView } from './MainView'; -import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import -import { PingManager } from '../util/PingManager'; import './global/globalScripts'; +import { MainView } from './MainView'; +import { BranchingTrailManager } from '../util/BranchingTrailManager'; dotenv.config(); AssignAllExtensions(); @@ -50,6 +51,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; document.cookie = `loadtime=${loading};${expires};path=/`; new TrackMovements(); new ReplayMovements(); + new BranchingTrailManager({}); new PingManager(); root.render(<MainView />); }, 0); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index b3faff442..4fb2ac279 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -62,8 +62,7 @@ h1, pointer-events: none; } -.mainView-container, -.mainView-container-Dark { +.mainView-container { width: 100%; height: 100%; position: absolute; @@ -74,58 +73,12 @@ h1, touch-action: none; } -.mainView-container, -.mainView-container-Dark { - .lm_header .lm_tab { - padding: 0px; - opacity: 0.7; - box-shadow: none; - height: 25px; - border-bottom: black solid; - } -} - .mainView-container { color: $dark-gray; .lm_goldenlayout { background: $medium-gray; } - - .lm_title { - background: $light-gray; - color: $dark-gray; - } -} - -.mainView-container-Dark { - color: $light-gray; - - .lm_goldenlayout { - background: $medium-gray; - } - - .lm_title { - background: $dark-gray; - color: unset; - } - - .marquee { - border-color: $white; - } - - #search-input { - background: $light-gray; - } - - .contextMenu-cont, - .contextMenu-item { - background: $dark-gray; - } - - .contextMenu-item:hover { - background: $medium-gray; - } } .mainView-dashboardArea { @@ -143,6 +96,7 @@ h1, top: 0; } +.mainView-propertiesDragger-minified, .mainView-propertiesDragger { //background-color: rgb(140, 139, 139); background-color: $light-gray; @@ -155,9 +109,9 @@ h1, border-bottom-left-radius: 10px; border-right: unset; z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that - display: flex; align-items: center; padding: 4px; + display: flex; .mainView-propertiesDragger-icon { width: 10px; @@ -171,9 +125,11 @@ h1, cursor: grab; } } +.mainView-propertiesDragger-minified { + width: 10px; +} -.mainView-innerContent, -.mainView-innerContent-Dark { +.mainView-innerContent { display: contents; flex-direction: row; position: relative; @@ -194,11 +150,11 @@ h1, left: 0; position: absolute; z-index: 2; - background-color: linen; //$light-gray; + // background-color: linen; //$light-gray; - .editable-title { - background-color: linen; //$light-gray; - } + // .editable-title { + // background-color: linen; //$light-gray; + // } } } @@ -206,44 +162,6 @@ h1, background-color: $light-gray; } -.mainView-innerContent-Dark { - .propertiesView { - background-color: #252525; - - input { - background-color: $medium-gray; - } - - .propertiesView-sharingTable { - background-color: $medium-gray; - } - - .editable-title { - background-color: $medium-gray; - } - - .propertiesView-field { - background-color: $medium-gray; - } - } - - .mainView-propertiesDragger, - .mainView-libraryHandle { - background: #353535; - } -} - -.mainView-container-Dark { - .contextMenu-cont { - background: $medium-gray; - color: $white; - - input::placeholder { - color: $white; - } - } -} - .mainView-leftMenuPanel { min-width: var(--menuPanelWidth); border-right: $standard-border; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8565941fd..d6f5d63fe 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -9,15 +9,15 @@ import { observer } from 'mobx-react'; import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { ScriptField } from '../../fields/ScriptField'; import { DocCast, StrCast } from '../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; +import { emptyFunction, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { CaptureManager } from '../util/CaptureManager'; import { DocumentManager } from '../util/DocumentManager'; +import { DragManager } from '../util/DragManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; @@ -43,7 +43,6 @@ import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; import { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } from './global/globalCssVariables.scss'; -import { Colors } from './global/globalEnums'; import { KeyManager } from './GlobalKeyHandler'; import { InkTranscription } from './InkTranscription'; import { LightboxView } from './LightboxView'; @@ -59,6 +58,8 @@ import GenerativeFill from './nodes/generativeFill/GenerativeFill'; import { ImageBox } from './nodes/ImageBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; +import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu'; +import { MapBox } from './nodes/MapBox/MapBox'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { OverlayView } from './OverlayView'; @@ -76,7 +77,6 @@ export class MainView extends React.Component { public static Live: boolean = false; private _docBtnRef = React.createRef<HTMLDivElement>(); - @observable public LastButton: Opt<Doc>; @observable private _windowWidth: number = 0; @observable private _windowHeight: number = 0; @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row) @@ -112,9 +112,6 @@ export class MainView extends React.Component { @computed private get userDoc() { return Doc.UserDoc(); } - @computed private get colorScheme() { - return StrCast(Doc.ActiveDashboard?.colorScheme); - } @observable mainDoc: Opt<Doc>; @computed private get mainContainer() { if (window.location.pathname.startsWith('/doc/') && Doc.CurrentUserEmail === 'guest') { @@ -144,6 +141,12 @@ export class MainView extends React.Component { mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight(); componentDidMount() { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'text/javascript'); + scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap'); + scriptTag.async = true; + scriptTag.defer = true; + document.body.appendChild(scriptTag); document.getElementById('root')?.addEventListener('scroll', e => (ele => (ele.scrollLeft = ele.scrollTop = 0))(document.getElementById('root')!)); const ele = document.getElementById('loader'); const prog = document.getElementById('dash-progress'); @@ -528,7 +531,6 @@ export class MainView extends React.Component { window.addEventListener('beforeunload', DocServer.UPDATE_SERVER_CACHE); window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page window.addEventListener('dragover', e => e.preventDefault(), false); - // document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined)); document.addEventListener('pointerdown', this.globalPointerDown, true); document.addEventListener('pointermove', this.globalPointerMove, true); document.addEventListener('pointerup', this.globalPointerClick, true); @@ -571,11 +573,7 @@ export class MainView extends React.Component { Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); }; - @observable _exploreMode = false; - @computed get exploreMode() { - return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); - } - waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined); + waitForDoubleClick = () => (DocumentView.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); @@ -702,7 +700,7 @@ export class MainView extends React.Component { switch (whereFields[0]) { case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location); case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); - case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, undefined, "dontSelectOnActivate"); // bcz: hack! mark the toggle so that it won't be selected on activation- this is needed so that the backlinks menu can toggle views of targets on and off without selecting them case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue); } }; @@ -746,7 +744,7 @@ export class MainView extends React.Component { @computed get leftMenuPanel() { return ( - <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}> + <div key="menu" className="mainView-leftMenuPanel" style={{ background: SettingsManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}> <DocumentView Document={Doc.MyLeftSidebarMenu} DataDoc={undefined} @@ -799,24 +797,32 @@ export class MainView extends React.Component { return ( <> {this._hideUI ? null : this.leftMenuPanel} - <div key="inner" className={`mainView-innerContent${this.colorScheme}`}> + <div key="inner" className="mainView-innerContent"> {this.flyout} <div className="mainView-libraryHandle" - style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} + style={{ background: SettingsManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}> - <FontAwesomeIcon icon="chevron-left" color={StrCast(Doc.UserDoc().userColor)} style={{ opacity: '50%' }} size="sm" /> + <FontAwesomeIcon icon="chevron-left" color={SettingsManager.userColor} style={{ opacity: '50%' }} size="sm" /> </div> <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}> {this.dockingContent} {this._hideUI ? null : ( - <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background: 'linen' }}> - <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" /> + <div + className={`mainView-propertiesDragger${this.propertiesWidth() < 10 ? '-minified' : ''}`} + key="props" + onPointerDown={this.onPropertiesPointerDown} + style={{ background: SettingsManager.userVariantColor, right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SettingsManager.userColor} size="sm" /> </div> )} - <div className="properties-container" style={{ width: this.propertiesWidth() }}> - {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />} + <div className="properties-container" style={{ width: this.propertiesWidth(), color: SettingsManager.userColor }}> + { + <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}> + <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} /> + </div> + } </div> </div> </div> @@ -838,7 +844,7 @@ export class MainView extends React.Component { ).observe(r); }} style={{ - color: this.colorScheme === ColorScheme.Dark ? 'rgb(205,205,205)' : 'black', + color: 'black', height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`, width: '100%', }}> @@ -856,11 +862,11 @@ export class MainView extends React.Component { //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); this._sidebarContent.proto = DocCast(button.target); - this.LastButton = button; + DocumentView.LastPressedSidebarBtn = button; }); closeFlyout = action(() => { - this.LastButton = undefined; + DocumentView.LastPressedSidebarBtn = undefined; this._panelContent = 'none'; this._sidebarContent.proto = undefined; this._leftMenuFlyoutWidth = 0; @@ -878,7 +884,7 @@ export class MainView extends React.Component { @computed get docButtons() { return !Doc.MyDockedBtns ? null : ( - <div className="mainView-docButtons" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }} ref={this._docBtnRef}> + <div className="mainView-docButtons" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }} ref={this._docBtnRef}> <CollectionLinearView Document={Doc.MyDockedBtns} DataDoc={undefined} @@ -908,19 +914,22 @@ export class MainView extends React.Component { childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> - {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>} + {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>} </div> ); } @computed get snapLines() { - return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : ( + SnappingManager.GetIsDragging(); + const dragged = DragManager.docsBeingDragged.lastElement(); + const dragPar = dragged ? DocumentManager.Instance.getDocumentView(dragged)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined; + return !dragPar?.rootDoc.freeform_snapLines ? null : ( <div className="mainView-snapLines"> <svg style={{ width: '100%', height: '100%' }}> {SnappingManager.horizSnapLines().map((l, i) => ( - <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.rootDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} /> ))} {SnappingManager.vertSnapLines().map((l, i) => ( - <line key={i} y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + <line key={i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.rootDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} /> ))} </svg> </div> @@ -955,14 +964,50 @@ export class MainView extends React.Component { @computed get linkDocPreview() { return LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null; } + @observable mapBoxHackBool = false; + @computed get mapBoxHack() { + return this.mapBoxHackBool ? null : ( + <MapBox + ref={action((r: any) => r && (this.mapBoxHackBool = true))} + fieldKey="data" + select={returnFalse} + isSelected={returnFalse} + Document={this.headerBarDoc} + DataDoc={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + docViewPath={returnEmptyDoclist} + styleProvider={DefaultStyleProvider} + rootSelected={returnTrue} + addDocument={returnFalse} + removeDocument={returnFalse} + fitContentsToBox={returnTrue} + isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events) + isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive + ScreenToLocalTransform={Transform.Identity} + childHideResizeHandles={returnTrue} + childDragAction="move" + dontRegisterView={true} + PanelWidth={this.headerBarDocWidth} + PanelHeight={this.headerBarDocHeight} + renderDepth={0} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + ); + } render() { return ( <div - className={`mainView-container ${this.colorScheme}`} + className="mainView-container" style={{ - color: StrCast(Doc.UserDoc().userColor), - background: StrCast(Doc.UserDoc().userBackgroundColor), + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, }} onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)} ref={r => { @@ -1010,6 +1055,7 @@ export class MainView extends React.Component { <ContextMenu /> <RadialMenu /> <AnchorMenu /> + <MapAnchorMenu /> <DashFieldViewMenu /> <MarqueeOptionsMenu /> <TimelineMenu /> @@ -1018,6 +1064,7 @@ export class MainView extends React.Component { {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> <OverlayView /> + {this.mapBoxHack} <GPTPopup key="gptpopup" /> <GenerativeFill imageEditorOpen={ImageBox.imageEditorOpen} imageEditorSource={ImageBox.imageEditorSource} imageRootDoc={ImageBox.imageRootDoc} addDoc={ImageBox.addDoc} /> {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */} @@ -1029,3 +1076,12 @@ export class MainView extends React.Component { ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); +ScriptingGlobals.add(function createNewPresentation() { + return MainView.Instance.createNewPresentation(); +}, 'creates a new presentation when called'); +ScriptingGlobals.add(function openPresentation(pres: Doc) { + return MainView.Instance.openPresentation(pres); +}, 'creates a new presentation when called'); +ScriptingGlobals.add(function createNewFolder() { + return MainView.Instance.createNewFolder(); +}, 'creates a new folder in myFiles when called'); diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 42df99864..67b722e7a 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; -import './MainViewModal.scss'; +import { isDark } from 'browndash-components'; import { observer } from 'mobx-react'; +import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { StrCast } from '../../fields/Types'; -import { isDark } from 'browndash-components'; -import { Colors } from './global/globalEnums'; +import { SettingsManager } from '../util/SettingsManager'; +import './MainViewModal.scss'; export interface MainViewOverlayProps { isDisplayed: boolean; @@ -45,7 +45,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> { className="overlay" onClick={this.props?.closeOnExternalClick} style={{ - backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? "#DFDFDF30" : "#32323230", + backgroundColor: isDark(SettingsManager.userColor) ? '#DFDFDF30' : '#32323230', ...(p.overlayStyle || {}), }} /> diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 3d8d569fa..a958607de 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -10,7 +10,7 @@ import { unimplementedFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { FollowLinkScript } from '../util/LinkFollower'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import './MarqueeAnnotator.scss'; import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @@ -55,7 +55,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping'); } }; - AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)); + AnchorMenu.Instance.OnClick = undoable((e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation'); AnchorMenu.Instance.OnAudio = unimplementedFunction; AnchorMenu.Instance.Highlight = this.highlight; AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true); diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 62aa4d1d4..c174befc0 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,4 +1,3 @@ - import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -17,6 +16,7 @@ import { LightboxView } from './LightboxView'; import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; import './OverlayView.scss'; import { DefaultStyleProvider } from './StyleProvider'; +const _global = (window /* browser */ || global) /* node */ as any; export type OverlayDisposer = () => void; @@ -116,6 +116,20 @@ export class OverlayView extends React.Component { super(props); if (!OverlayView.Instance) { OverlayView.Instance = this; + new _global.ResizeObserver( + action((entries: any) => { + for (const entry of entries) { + DocListCast(Doc.MyOverlayDocs?.data).forEach(doc => { + if (NumCast(doc.overlayX) > entry.contentRect.width - 10) { + doc.overlayX = entry.contentRect.width - 10; + } + if (NumCast(doc.overlayY) > entry.contentRect.height - 10) { + doc.overlayY = entry.contentRect.height - 10; + } + }); + } + }) + ).observe(window.document.body); } } diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx new file mode 100644 index 000000000..749eb08a2 --- /dev/null +++ b/src/client/views/Palette.tsx @@ -0,0 +1,69 @@ +import { IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc } from '../../fields/Doc'; +import { NumCast } from '../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, emptyPath } from '../../Utils'; +import { Transform } from '../util/Transform'; +import { DocumentView } from './nodes/DocumentView'; +import './Palette.scss'; + +export interface PaletteProps { + x: number; + y: number; + thumb: number[]; + thumbDoc: Doc; +} + +@observer +export default class Palette extends React.Component<PaletteProps> { + private _selectedDisposer?: IReactionDisposer; + @observable private _selectedIndex: number = 0; + + componentDidMount = () => { + this._selectedDisposer = reaction( + () => NumCast(this.props.thumbDoc.selectedIndex), + i => (this._selectedIndex = i), + { fireImmediately: true } + ); + }; + + componentWillUnmount = () => { + this._selectedDisposer?.(); + }; + + render() { + return ( + <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}> + <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}> + <div className="palette-thumbContent" style={{ transform: `translate(-${this._selectedIndex * 50 + 10}px, 0px)` }}> + <DocumentView + Document={this.props.thumbDoc} + DataDoc={undefined} + addDocument={undefined} + addDocTab={returnFalse} + rootSelected={returnTrue} + pinToPres={emptyFunction} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => window.screen.width} + PanelHeight={() => window.screen.height} + renderDepth={0} + isDocumentActive={returnTrue} + isContentActive={emptyFunction} + focus={emptyFunction} + docViewPath={returnEmptyDoclist} + styleProvider={returnEmptyString} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> + </div> + </div> + </div> + ); + } +} diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss index 7be765ea9..82488c750 100644 --- a/src/client/views/PreviewCursor.scss +++ b/src/client/views/PreviewCursor.scss @@ -1,4 +1,3 @@ -.previewCursor-Dark, .previewCursor { color: black; position: absolute; @@ -9,7 +8,3 @@ opacity: 1; z-index: 1001; } - -.previewCursor-Dark { - color: white; -} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 82d2bff56..e3a43d45f 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -1,9 +1,8 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../fields/Doc'; -import { StrCast } from '../../fields/Types'; -import { returnFalse } from '../../Utils'; +import { Doc, Opt } from '../../fields/Doc'; +import { lightOrDark, returnFalse } from '../../Utils'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { ImageUtils } from '../util/Import & Export/ImageUtils'; import { Transform } from '../util/Transform'; @@ -21,6 +20,7 @@ export class PreviewCursor extends React.Component<{}> { static _slowLoadDocuments?: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; + public static Doc: Opt<Doc>; constructor(props: any) { super(props); document.addEventListener('keydown', this.onKeyPress); @@ -178,7 +178,12 @@ export class PreviewCursor extends React.Component<{}> { } render() { return !PreviewCursor._clickPoint || !PreviewCursor.Visible ? null : ( - <div className={`previewCursor${StrCast(Doc.ActiveDashboard?.colorScheme)}`} onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()} style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}> + <div + className="previewCursor" + onBlur={this.onBlur} + tabIndex={0} + ref={e => e?.focus()} + style={{ color: lightOrDark(PreviewCursor.Doc?.backgroundColor ?? 'white'), transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}> I </div> ); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 8cae34d7d..d1561fd67 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -32,9 +32,7 @@ import { TfiBarChart } from 'react-icons/tfi'; import { CiGrid31 } from 'react-icons/ci'; import { RxWidth } from 'react-icons/rx'; import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; +import { SettingsManager } from '../util/SettingsManager'; enum UtilityButtonState { Default, @@ -59,8 +57,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !targetDoc ? null : ( <Toggle toggleStatus={BoolCast(targetDoc[property])} + tooltip={tooltip(BoolCast(targetDoc[property]))} text={label(targetDoc?.[property])} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} icon={icon(targetDoc?.[property] as any)} iconPlacement={'left'} align={'flex-start'} @@ -167,9 +166,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get forceActiveButton() { //select text return this.propertyToggleBtn( - on => (on ? 'INACTIVE INTERACTION' : 'ACTIVE INTERACTION'), + on => (on ? 'SELECT TO INTERACT' : 'ALWAYS INTERACTIVE'), '_forceActive', - on => `${on ? 'Select to activate' : 'Contents always active'} `, + on => `${on ? 'Document must be selected to interact with its contents' : 'Contents always active (respond to click/drag events)'} `, on => <MdTouchApp /> // 'eye' ); } @@ -212,9 +211,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get layout_fitWidthButton() { return this.propertyToggleBtn( - on => (on ? 'RESTRICT WIDTH' : 'FIT WIDTH'), //'Fit\xA0Width', + on => (on ? 'SCALED VIEW' : 'READING VIEW'), //'Fit\xA0Width', '_layout_fitWidth', - on => `${on ? "Don't" : 'Do'} fit content to width of container`, + on => + on + ? "Scale document so it's width and height fit container (no effect when document is viewed on freeform canvas)" + : "Scale document so it's width fits container and its height expands/contracts to fit available space (no effect when document is viewed on freeform canvas)", on => (on ? <AiOutlineColumnWidth /> : <RxWidth />) // 'arrows-alt-h' ); } @@ -379,10 +381,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { <Dropdown tooltip={'Choose onClick behavior'} items={items} + closeOnSelect={true} selectedVal={this.onClickVal} setSelectedVal={val => this.handleOptionChange(val as string)} title={'Choose onClick behaviour'} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} dropdownType={DropdownType.SELECT} type={Type.SEC} fillWidth @@ -514,7 +517,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} {toggle(this.maskButton, { display: !isInk ? 'none' : '' })} {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })} - {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })} + {toggle(this.chromeButton, { display: isNovice ? 'none' : '' })} {toggle(this.gridButton, { display: !isCollection ? 'none' : '' })} {/* {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })} */} {toggle(this.snapButton, { display: !isCollection ? 'none' : '' })} diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index da4438481..b1e99c3f5 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -1,3 +1,4 @@ +import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; @@ -5,6 +6,8 @@ import { Cast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { LinkMenu } from './linking/LinkMenu'; import { OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocBacklinksSelector.scss'; @@ -18,20 +21,21 @@ type PropertiesDocBacklinksSelectorProps = { @observer export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> { - getOnClick = (link: Doc) => { + getOnClick = action((link: Doc) => { const linkAnchor = this.props.Document; const other = LinkManager.getOppositeAnchor(link, linkAnchor); const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; - + LinkManager.currentLink = link; if (otherdoc) { otherdoc.hidden = false; this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); + CollectionDockingView.Instance?.endUndoBatch(); } - }; + }); render() { return !SelectionManager.Views().length ? null : ( - <div className="propertiesDocBacklinksSelector"> + <div className="propertiesDocBacklinksSelector" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>} <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} /> </div> diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx index 6fab0168b..bd586b2f9 100644 --- a/src/client/views/PropertiesSection.tsx +++ b/src/client/views/PropertiesSection.tsx @@ -5,10 +5,11 @@ import { observer } from 'mobx-react'; import './PropertiesSection.scss'; import { Doc } from '../../fields/Doc'; import { StrCast } from '../../fields/Types'; +import { SettingsManager } from '../util/SettingsManager'; export interface PropertiesSectionProps { title: string; - content?: JSX.Element | string | null; + children?: JSX.Element | string | null; isOpen: boolean; setIsOpen: (bool: boolean) => any; inSection?: boolean; @@ -19,21 +20,21 @@ export interface PropertiesSectionProps { @observer export class PropertiesSection extends React.Component<PropertiesSectionProps> { @computed get color() { - return StrCast(Doc.UserDoc().userColor); + return SettingsManager.userColor; } @computed get backgroundColor() { - return StrCast(Doc.UserDoc().userBackgroundColor); + return SettingsManager.userBackgroundColor; } @computed get variantColor() { - return StrCast(Doc.UserDoc().userVariantColor); + return SettingsManager.userVariantColor; } @observable isDouble: boolean = false; render() { - if (this.props.content === undefined || this.props.content === null) return null; + if (this.props.children === undefined || this.props.children === null) return null; else return ( <div className="propertiesView-section" onPointerEnter={action(() => this.props.setInSection && this.props.setInSection(true))} onPointerLeave={action(() => this.props.setInSection && this.props.setInSection(false))}> @@ -49,7 +50,8 @@ export class PropertiesSection extends React.Component<PropertiesSectionProps> { this.props.setIsOpen(!this.props.isOpen); })} style={{ - background: this.props.isOpen ? this.variantColor : this.backgroundColor, + background: this.variantColor, + // this.props.isOpen ? this.variantColor : this.backgroundColor, color: this.color, }}> {this.props.title} @@ -57,7 +59,7 @@ export class PropertiesSection extends React.Component<PropertiesSectionProps> { <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> - {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.content}</div>} + {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.children}</div>} </div> ); } diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index a0b054851..2da2ec568 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -23,6 +23,14 @@ padding: 10px; font-size: 24px; font-weight: bold; + display: flex; + flex-direction: row; + } + .propertiesView-titleExtender { + text-overflow: ellipsis; + max-width: 100%; + white-space: pre; + overflow: hidden; } overflow-x: hidden; @@ -33,6 +41,19 @@ padding: 5px 10px; } + .propertiesView-propAndInfoGrouping { + display: flex; + } + + .propertiesView-info { + margin-top: -5; + float: right; + font-size: 20; + path { + fill: white !important; + } + } + .propertiesView-sharing { //border-bottom: 1px solid black; //padding: 8.5px; @@ -251,10 +272,10 @@ } } -.propertiesView-presentationTrails { - //border-bottom: 1px solid black; - //padding: 8.5px; -} +//.propertiesView-presentationTrails { +//border-bottom: 1px solid black; +//padding: 8.5px; +//} .inking-button { display: flex; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 82ab54787..04a948a27 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -3,14 +3,12 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; -import { Button, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; +import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; import { concat } from 'lodash'; -import { Lambda, action, computed, observable } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import * as Icons from 'react-icons/bs'; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs" -import { GrCircleInformation } from 'react-icons/gr'; -import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc'; import { AclAdmin, DocAcl, DocData, Height, Width } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; @@ -18,31 +16,29 @@ import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; -import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util'; -import { DocumentType } from '../documents/DocumentTypes'; +import { GetEffectiveAcl, normalizeEmail, SharingPermissions } from '../../fields/util'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils'; +import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { UndoManager, undoBatch, undoable } from '../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; import { InkStrokeProperties } from './InkStrokeProperties'; +import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; +import { KeyValueBox } from './nodes/KeyValueBox'; +import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import { PropertiesSection } from './PropertiesSection'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; -import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; -import { KeyValueBox } from './nodes/KeyValueBox'; -import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; -import { SettingsManager } from '../util/SettingsManager'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -56,6 +52,12 @@ interface PropertiesViewProps { export class PropertiesView extends React.Component<PropertiesViewProps> { private _widthUndo?: UndoManager.Batch; + public static Instance: PropertiesView | undefined; + constructor(props: any) { + super(props); + PropertiesView.Instance = this; + } + @computed get MAX_EMBED_HEIGHT() { return 200; } @@ -79,6 +81,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @observable layoutFields: boolean = false; + @observable layoutDocAcls: boolean = false; @observable openOptions: boolean = true; @observable openSharing: boolean = true; @@ -90,15 +93,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openTransform: boolean = true; @observable openFilters: boolean = false; - /** - * autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open - */ - private selectedDocListenerDisposer: Opt<Lambda>; - - // @observable selectedUser: string = ""; - // @observable addButtonPressed: boolean = false; - @observable layoutDocAcls: boolean = false; - //Pres Trails booleans: @observable openPresTransitions: boolean = true; @observable openPresProgressivize: boolean = false; @@ -108,19 +102,29 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable inOptions: boolean = false; @observable _controlButton: boolean = false; + private _disposers: { [name: string]: IReactionDisposer } = {}; componentDidMount() { - this.selectedDocListenerDisposer?.(); - // this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); + this._disposers.link = reaction( + () => LinkManager.currentLink, + link => { + link && this.CloseAll(); + link && (this.openLinks = true); + }, + { fireImmediately: true } + ); } componentWillUnmount() { - this.selectedDocListenerDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } + @computed get isStack() { + return [CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any); + } rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[Width](), this.props.width - 20)); rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[Width]() ? Math.min(this.selectedDoc?.[Height](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @@ -191,7 +195,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }); rows.push( - <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: 'white', textAlign: 'center' }}> + <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} /> </div> ); @@ -367,7 +371,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <IconButton icon={<FontAwesomeIcon icon={'ellipsis-h'} />} size={Size.XSMALL} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} onClick={action(() => { if (this.selectedDocumentView || this.selectedDoc) { SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); @@ -517,7 +521,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div> <br></br> Individuals with Access to this Document </div> - <div className="propertiesView-sharingTable" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}> + <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> {<div> {individualTableEntries}</div>} </div> {groupTableEntries.length > 0 ? ( @@ -525,7 +529,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div> <br></br> Groups with Access to this Document </div> - <div className="propertiesView-sharingTable" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}> + <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> {<div> {groupTableEntries}</div>} </div> </div> @@ -545,22 +549,42 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { toggleCheckbox = () => (this.layoutFields = !this.layoutFields); @computed get color() { - return StrCast(Doc.UserDoc().userColor); + return SettingsManager.userColor; } @computed get backgroundColor() { - return StrCast(Doc.UserDoc().userBackgroundColor); + return SettingsManager.userBackgroundColor; } @computed get variantColor() { - return StrCast(Doc.UserDoc().userVariantColor); + return SettingsManager.userVariantColor; } @computed get editableTitle() { const titles = new Set<string>(); - const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title); SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); - return <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth />; + const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title); + return ( + <div> + <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth /> + {LinkManager.currentLinkAnchor ? ( + <p className="propertiesView-titleExtender"> + <> + <b>Anchor:</b> + {LinkManager.currentLinkAnchor.title} + </> + </p> + ) : null} + {LinkManager.currentLink?.title ? ( + <p className="propertiesView-titleExtender"> + <> + <b>Link:</b> + {LinkManager.currentLink.title} + </> + </p> + ) : null} + </div> + ); } @computed get currentType() { @@ -570,7 +594,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <div> - <div className="propertiesView-wordType">Type</div> + Type + {/* <div className = "propertiesView-wordType">Type</div> */} <div className="currentType"> <div className="currentType-icon">{this.currentComponent}</div> @@ -586,9 +611,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (iconName) { const Icon = Icons[iconName as keyof typeof Icons]; return <Icon />; - } else { - return <Icons.BsFillCollectionFill />; } + return <Icons.BsFillCollectionFill />; } @undoBatch @@ -673,7 +697,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { marginLeft: title === '∠:' ? '39px' : '', }}> <div className="inputBox-title"> {title} </div> - <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} onKeyPress={e => e.stopPropagation()} /> + <input + className="inputBox-input" + type="text" + value={value} + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => setter(e.target.value)} + onKeyPress={e => e.stopPropagation()} + /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}> <FontAwesomeIcon icon="caret-up" size="sm" /> @@ -752,39 +783,39 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get shapeXps() { - return this.getField('x'); + return NumCast(this.selectedDoc?.x); } @computed get shapeYps() { - return this.getField('y'); + return NumCast(this.selectedDoc?.y); } @computed get shapeHgt() { - return this.getField('_height'); + return NumCast(this.selectedDoc?._height); } @computed get shapeWid() { - return this.getField('_width'); + return NumCast(this.selectedDoc?._width); } set shapeXps(value) { - this.selectedDoc && (this.selectedDoc.x = Number(value)); + this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } set shapeYps(value) { - this.selectedDoc && (this.selectedDoc.y = Number(value)); + this.selectedDoc && (this.selectedDoc.y = Math.round(value * 100) / 100); } set shapeWid(value) { - this.selectedDoc && (this.selectedDoc._width = Number(value)); + this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } set shapeHgt(value) { - this.selectedDoc && (this.selectedDoc._height = Number(value)); + this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } @computed get hgtInput() { return this.inputBoxDuo( 'hgt', this.shapeHgt, - undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height'), + undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = +val), 'set height'), 'H:', 'wid', this.shapeWid, - undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width'), + undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = +val), 'set width'), 'W:' ); } @@ -792,11 +823,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.inputBoxDuo( 'Xps', this.shapeXps, - undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord'), + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = +val), 'set x coord'), 'X:', 'Yps', this.shapeYps, - undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord'), + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = +val), 'set y coord'), 'Y:' ); } @@ -930,7 +961,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { regInput = (key: string, value: any, setter: (val: string) => {}) => { return ( <div className="inputBox"> - <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} /> + <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}> <FontAwesomeIcon icon="caret-up" size="sm" /> @@ -944,7 +975,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }; @action - onDoubleClick = () => { + CloseAll = () => { this.openContexts = false; this.openLinks = false; this.openOptions = false; @@ -966,6 +997,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <input className="width-range" type="range" + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} defaultValue={Number(this.widthStk)} min={1} max={100} @@ -990,6 +1022,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="width-range" type="range" defaultValue={this.markScal} + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} min={0} max={10} onChange={action(e => (this.markScal = +e.target.value))} @@ -1034,11 +1067,31 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } - getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any) => { + _sliderBatch: UndoManager.Batch | undefined; + setFinalNumber = () => { + this._sliderBatch?.end(); + }; + getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => { return ( - <div> + <div key={label + this.selectedDoc?.title}> <NumberInput formLabel={label} formLabelPlacement={'left'} type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} /> - <Slider multithumb={false} color={this.color} size={Size.XSMALL} min={min} max={max} unit={unit} number={number} setNumber={setNumber} fillWidth /> + <Slider + key={label} + onPointerDown={e => (this._sliderBatch = UndoManager.StartBatch('slider ' + label))} + multithumb={false} + color={this.color} + size={Size.XSMALL} + min={min} + max={max} + autorangeMinVal={autorangeMinVal} + autorange={autorange} + number={number} + unit={unit} + decimals={1} + setFinalNumber={this.setFinalNumber} + setNumber={setNumber} + fillWidth + /> </div> ); }; @@ -1046,81 +1099,43 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get transformEditor() { return ( <div className="transform-editor"> + {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), (val: number) => !isNaN(val) && (this.selectedDoc!.gridGap = val))} + {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.xMargin = val))} {this.isInk ? this.controlPointsButton : null} - {this.getNumber( - 'Height', - ' px', - 0, - 1000, - Number(this.shapeHgt), - undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height') - )} - {this.getNumber( - 'Width', - ' px', - 0, - 1000, - Number(this.shapeWid), - undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width') - )} - {this.getNumber( - 'X Coordinate', - ' px', - -2000, - 2000, - Number(this.shapeXps), - undoable((val: string) => !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord') - )} - {this.getNumber( - 'Y Coordinate', - ' px', - -2000, - 2000, - Number(this.shapeYps), - undoable((val: string) => !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord') - )} + {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, (val: number) => !isNaN(val) && (this.shapeWid = val), 1000, 1)} + {this.getNumber('Height', ' px', 0, Math.max(1000, this.shapeHgt), this.shapeHgt, (val: number) => !isNaN(val) && (this.shapeHgt = val), 1000, 1)} + {this.getNumber('X', ' px', this.shapeXps - 500, this.shapeXps + 500, this.shapeXps, (val: number) => !isNaN(val) && (this.shapeXps = val), 1000)} + {this.getNumber('Y', ' px', this.shapeYps - 500, this.shapeYps + 500, this.shapeYps, (val: number) => !isNaN(val) && (this.shapeYps = val), 1000)} </div> ); } @computed get optionsSubMenu() { return ( - <PropertiesSection - title="Options" - content={<PropertiesButtons />} - inSection={this.inOptions} - isOpen={this.openOptions} - setInSection={bool => (this.inOptions = bool)} - setIsOpen={bool => (this.openOptions = bool)} - onDoubleClick={() => this.onDoubleClick()} - /> + <PropertiesSection title="Options" inSection={this.inOptions} isOpen={this.openOptions} setInSection={bool => (this.inOptions = bool)} setIsOpen={bool => (this.openOptions = bool)} onDoubleClick={this.CloseAll}> + <PropertiesButtons /> + </PropertiesSection> ); } @computed get sharingSubMenu() { return ( - <PropertiesSection - title="Sharing & Permissions" - content={ - <> - {/* <div className="propertiesView-buttonContainer"> */} - <div className="propertiesView-acls-checkbox"> - Layout Permissions - <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> - </div> - {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> - <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> - <FontAwesomeIcon icon="redo-alt" size="1x" /> - </button> + <PropertiesSection title="Sharing and Permissions" isOpen={this.openSharing} setIsOpen={bool => (this.openSharing = bool)} onDoubleClick={() => this.CloseAll()}> + <> + {/* <div className="propertiesView-buttonContainer"> */} + <div className="propertiesView-acls-checkbox"> + Layout Permissions + <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> + </div> + {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> + <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> + <FontAwesomeIcon icon="redo-alt" size="1x" /> + </button> </Tooltip> */} - {/* </div> */} - {this.sharingTable} - </> - } - isOpen={this.openSharing} - setIsOpen={bool => (this.openSharing = bool)} - onDoubleClick={() => this.onDoubleClick()} - /> + {/* </div> */} + {this.sharingTable} + </> + </PropertiesSection> ); } @@ -1150,17 +1165,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get filtersSubMenu() { return ( - <PropertiesSection - title="Filters" - content={ - <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}> - <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} /> - </div> - } - isOpen={this.openFilters} - setIsOpen={bool => (this.openFilters = bool)} - onDoubleClick={() => this.onDoubleClick()} - /> + <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => (this.openFilters = bool)} onDoubleClick={() => this.CloseAll()}> + <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}> + <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} /> + </div> + </PropertiesSection> ); } @@ -1169,42 +1178,46 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <> - <PropertiesSection title="Appearance" content={this.isInk ? this.appearanceEditor : null} isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.onDoubleClick()} /> - <PropertiesSection title="Transform" content={this.transformEditor} isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.onDoubleClick()} /> + <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.CloseAll()}> + {this.isInk ? this.appearanceEditor : null} + </PropertiesSection> + <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.CloseAll()}> + {this.transformEditor} + </PropertiesSection> </> ); } @computed get fieldsSubMenu() { return ( - <PropertiesSection - title="Fields & Tags" - content={<div className="propertiesView-content fields">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>} - isOpen={this.openFields} - setIsOpen={bool => (this.openFields = bool)} - onDoubleClick={() => this.onDoubleClick()} - /> + <PropertiesSection title="Fields & Tags" isOpen={this.openFields} setIsOpen={bool => (this.openFields = bool)} onDoubleClick={() => this.CloseAll()}> + <div className="propertiesView-content fields">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div> + </PropertiesSection> ); } @computed get contextsSubMenu() { return ( - <PropertiesSection - title="Other Contexts" - content={this.contextCount > 0 ? this.contexts : 'There are no other contexts.'} - isOpen={this.openContexts} - setIsOpen={bool => (this.openContexts = bool)} - onDoubleClick={() => this.onDoubleClick()} - /> + <PropertiesSection title="Other Contexts" isOpen={this.openContexts} setIsOpen={bool => (this.openContexts = bool)} onDoubleClick={() => this.CloseAll()}> + {this.contextCount > 0 ? this.contexts : 'There are no other contexts.'} + </PropertiesSection> ); } @computed get linksSubMenu() { - return <PropertiesSection title="Linked To" content={this.linkCount > 0 ? this.links : 'There are no current links.'} isOpen={this.openLinks} setIsOpen={bool => (this.openLinks = bool)} onDoubleClick={() => this.onDoubleClick()} />; + return ( + <PropertiesSection title="Linked To" isOpen={this.openLinks} setIsOpen={bool => (this.openLinks = bool)} onDoubleClick={this.CloseAll}> + {this.linkCount > 0 ? this.links : 'There are no current links.'} + </PropertiesSection> + ); } @computed get layoutSubMenu() { - return <PropertiesSection title="Layout" content={this.layoutPreview} isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={() => this.onDoubleClick()} />; + return ( + <PropertiesSection title="Layout" isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={this.CloseAll}> + {this.layoutPreview} + </PropertiesSection> + ); } @computed get description() { @@ -1314,10 +1327,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }; onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { - if (e.key === 'Enter') { - this.setDescripValue(this.description); - document.getElementById('link_description_input')?.blur(); - } + // if (e.key === 'Enter') { + // this.setDescripValue(this.description); + // document.getElementById('link_description_input')?.blur(); + // } }; onSelectOutRelationship = () => { @@ -1369,6 +1382,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { get editRelationship() { return ( <input + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} autoComplete={'off'} id="link_relationship_input" value={StrCast(LinkManager.currentLink?.link_relationship)} @@ -1386,7 +1400,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <textarea autoComplete="off" - style={{ textAlign: 'left' }} + style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} id="link_description_input" value={StrCast(LinkManager.currentLink?.link_description)} onKeyDown={this.onDescriptionKey} @@ -1414,7 +1428,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <> - <div className="propertiesView-section" style={{ background: 'darkgray' }}> + <div className="propertiesView-section"> <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> <p>Relationship</p> {this.editRelationship} @@ -1458,7 +1472,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-section"> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}> <p>Follow by</p> - <select onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}> + <select + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} + value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}> <option value={undefined}>Default</option> <option value={OpenWhere.addLeft}>Opening in new left pane</option> <option value={OpenWhere.addRight}>Opening in new right pane</option> @@ -1473,7 +1490,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> <p>Animation</p> - <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> + <select + style={{ width: '100%', gridColumn: 2, color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} + value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> <option value="default">Default</option> {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( <option key={effect.toString()} value={effect.toString()}> @@ -1523,6 +1543,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </button> </div> <div className="propertiesView-input inline"> + <p>Play Target Video</p> + <button + style={{ background: !this.sourceAnchor?.followLinkVideo ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkVideo', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> <p>Zoom Text Selections</p> <button style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }} @@ -1588,7 +1618,7 @@ export class PropertiesView extends React.Component<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%' }} readOnly={true} type="number" value={zoom} /> + <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} 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'} /> @@ -1655,30 +1685,30 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView" style={{ - background: StrCast(Doc.UserDoc().userBackgroundColor), - color: StrCast(Doc.UserDoc().userColor), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, width: this.props.width, minWidth: this.props.width, }}> <div className="propertiesView-propAndInfoGrouping"> <div className="propertiesView-title" style={{ width: this.props.width }}> Properties - </div> - <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/')}> - <GrCircleInformation />{' '} + <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} /> + </div> </div> </div> <div className="propertiesView-name">{this.editableTitle}</div> <div className="propertiesView-type"> {this.currentType} </div> - {this.contextsSubMenu} + {this.optionsSubMenu} {this.linksSubMenu} - {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : this.linkProperties} + {!LinkManager.currentLink || !this.openLinks ? null : this.linkProperties} {this.inkSubMenu} - {this.optionsSubMenu} + {this.contextsSubMenu} {this.fieldsSubMenu} {isNovice ? null : this.sharingSubMenu} - {isNovice ? null : this.filtersSubMenu} + {this.filtersSubMenu} {isNovice ? null : this.layoutSubMenu} </div> ); @@ -1706,8 +1736,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresTransitions ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Transitions <div className="propertiesView-presentationTrails-title-icon"> @@ -1723,15 +1753,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Visibilty + <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Visibility <div className="propertiesView-presentationTrails-title-icon"> <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> - {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null} + {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null} </div> )} {!selectedItem ? null : ( @@ -1740,8 +1770,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresProgressivize ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Progressivize <div className="propertiesView-presentationTrails-title-icon"> @@ -1757,8 +1787,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openSlideOptions ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'} <div className="propertiesView-presentationTrails-title-icon"> diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 2bc2d5e6b..9966dbced 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { SettingsManager } from '../util/SettingsManager'; import { undoable } from '../util/UndoManager'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; @@ -245,17 +246,29 @@ export class ScriptingRepl extends React.Component { render() { return ( <div className="scriptingRepl-outerContainer"> - <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}> + <div className="scriptingRepl-commandsContainer" style={{ background: SettingsManager.userBackgroundColor }} ref={this.commandsRef}> {this.commands.map(({ command, result }, i) => { return ( - <div className="scriptingRepl-resultContainer" key={i}> - <div className="scriptingRepl-commandString">{command || <br />}</div> - <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div> + <div className="scriptingRepl-resultContainer" style={{ background: SettingsManager.userBackgroundColor }} key={i}> + <div className="scriptingRepl-commandString" style={{ background: SettingsManager.userBackgroundColor }}> + {command || <br />} + </div> + <div className="scriptingRepl-commandResult" style={{ background: SettingsManager.userBackgroundColor }}> + {<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />} + </div> </div> ); })} </div> - <input className="scriptingRepl-commandInput" onFocus={this.onFocus} onBlur={this.onBlur} value={this.commandString} onChange={this.onChange} onKeyDown={this.onKeyDown}></input> + <input + className="scriptingRepl-commandInput" + style={{ background: SettingsManager.userBackgroundColor }} // + onFocus={this.onFocus} + onBlur={this.onBlur} + value={this.commandString} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + /> </div> ); } diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index db273cc88..1e1b8e0e6 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -9,11 +9,11 @@ import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZer import { Docs, DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { LinkManager } from '../util/LinkManager'; +import { SearchUtil } from '../util/SearchUtil'; import { Transform } from '../util/Transform'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { SearchBox } from './search/SearchBox'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProvider'; import React = require('react'); @@ -43,7 +43,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { @computed get allMetadata() { const keys = new Map<string, FieldResult<Field>>(); DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => - SearchBox.documentKeys(doc) + SearchUtil.documentKeys(doc) .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase()) .map(key => keys.set(key, doc[key])) ); @@ -62,12 +62,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author))); return Array.from(keys.keys()).sort(); } - get filtersKey() { - return '_' + this.sidebarKey + '_childFilters'; - } - anchorMenuClick = (anchor: Doc) => { - const startup = StrListCast(this.props.rootDoc.childFilters) + anchorMenuClick = (anchor: Doc, filterExlusions?: string[]) => { + const startup = this.childFilters() .map(filter => filter.split(':')[0]) .join(' '); const target = Docs.Create.TextDocument(startup, { @@ -87,6 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { const taggedContent = this.childFilters() .filter(data => data.split(':')[0]) + .filter(data => !filterExlusions?.includes(data.split(':')[0])) .map(data => { const key = data.split(':')[0]; const val = Field.Copy(this.allMetadata.get(key)); @@ -150,8 +148,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }; makeDocUnfiltered = (doc: Doc) => { if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (this.props.layoutDoc[this.filtersKey]) { - this.props.layoutDoc[this.filtersKey] = new List<string>(); + if (this.childFilters()) { + // if any child filters exist, get rid of them + this.props.layoutDoc._childFilters = new List<string>(); } return true; } @@ -177,7 +176,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey); - childFilters = () => [...StrListCast(this.props.layoutDoc._childFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; + childFilters = () => StrListCast(this.props.layoutDoc._childFilters); layout_showTitle = () => 'title'; setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { @@ -187,25 +186,25 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }; render() { const renderTag = (tag: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`); + const active = this.childFilters().includes(`tags${Doc.FilterSep}${tag}${Doc.FilterSep}check`); return ( - <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}> + <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, undefined, e.shiftKey)}> {tag} </div> ); }; const renderMeta = (tag: string, dflt: FieldResult<Field>) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${dflt}:exists`); + 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.rootDoc, tag, dflt, 'exists', true, this.sidebarKey, e.shiftKey)}> + <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}> {tag} </div> ); }; const renderUsers = (user: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`author:${user}:check`); + const active = this.childFilters().includes(`author:${user}:check`); return ( - <div key={user} className={`sidebarAnnos-filterUser${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'author', user, 'check', true, this.sidebarKey, e.shiftKey)}> + <div key={user} className={`sidebarAnnos-filterUser${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'author', user, 'check', true, undefined, e.shiftKey)}> {user} </div> ); @@ -223,12 +222,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { height: '100%', }}> <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> - {this.allUsers.map(renderUsers)} + {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)))} </div> + <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> <CollectionStackingView {...this.props} diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index c06bb287e..f069e7e1b 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -18,10 +18,15 @@ cursor: default; } .styleProvider-filter { - right: 0; + right: 15; + .styleProvider-filterShift { + left: 0; + top: 0; + position: absolute; + } } .styleProvider-audio { - right: 15; + right: 30; } .styleProvider-lock:hover, .styleProvider-audio:hover, diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 24a269927..d7537bffb 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,9 +1,10 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { IconButton, Shadows, Size } from 'browndash-components'; +import { Dropdown, DropdownType, IconButton, IListItemProps, ListBox, ListItem, Popup, Shadows, Size, Type } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { extname } from 'path'; +import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; import { DashColor, lightOrDark, Utils } from '../../Utils'; @@ -14,17 +15,17 @@ import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { ColorScheme, SettingsManager } from '../util/SettingsManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; -import { TreeSort } from './collections/TreeView'; +import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; -import { MainView } from './MainView'; -import { DocumentViewProps } from './nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { SliderBox } from './nodes/SliderBox'; -import { BsArrowDown, BsArrowUp, BsArrowDownUp } from 'react-icons/bs'; import './StyleProvider.scss'; import React = require('react'); +import { PropertiesView } from './PropertiesView'; +import { FaFilter } from 'react-icons/fa'; export enum StyleProp { TreeViewIcon = 'treeView_Icon', @@ -51,10 +52,6 @@ export enum StyleProp { Highlighting = 'highlighting', // border highlighting } -function darkScheme() { - return Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark; -} - function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch( () => @@ -89,12 +86,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const isCaption = property.includes(':caption'); const isAnchor = property.includes(':anchor'); const isAnnotated = property.includes(':annotated'); + const isInk = () => doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !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 lockedPosition = () => doc && BoolCast(doc._lockedPosition); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); + 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); // prettier-ignore @@ -119,7 +118,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) { const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc); const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); - const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; + const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; if (highlightIndex) { return { @@ -132,7 +131,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } return undefined; case StyleProp.DocContents:return undefined; - case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; + case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); case StyleProp.HideLinkBtn:return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._text_fontSize, StrCast(Doc.UserDoc().fontSize))); @@ -144,6 +143,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.ShowTitle: return ( (doc && + !props?.LayoutTemplateString && !doc.presentation_targetDoc && !props?.LayoutTemplateString?.includes(KeyValueBox.name) && props?.layout_showTitle?.() !== '' && @@ -159,9 +159,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps '' ); case StyleProp.Color: - if (MainView.Instance.LastButton === doc) return SettingsManager.Instance.userBackgroundColor; - if (Doc.IsSystem(doc!)) return StrCast(Doc.UserDoc().userColor) - if (doc?.type === DocumentType.FONTICON) return Doc.UserDoc().userColor; + if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userBackgroundColor; + if (Doc.IsSystem(doc!)) return SettingsManager.userColor; + if (doc?.type === DocumentType.FONTICON) return SettingsManager.userColor; const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color)); if (docColor) return docColor; const docView = props?.DocumentView?.(); @@ -194,40 +194,40 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? 15 : 0; case StyleProp.BackgroundColor: { - if (MainView.Instance.LastButton === doc) return StrCast(Doc.UserDoc().userColor); // hack to indicate active menu panel item + if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item let docColor: Opt<string> = StrCast(doc?.[fieldKey + '_backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); // prettier-ignore switch (doc?.type) { case DocumentType.SLIDER: break; - case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? '' : ''); break; - case DocumentType.PRES: docColor = docColor || (darkScheme() ? 'transparent' : 'transparent'); break; + case DocumentType.PRESELEMENT: docColor = docColor || ""; break; + case DocumentType.PRES: docColor = docColor || 'transparent'; break; case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break; - case DocumentType.RTF: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.RTF: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break; case DocumentType.EQUATION: docColor = docColor || 'transparent'; break; - case DocumentType.LABEL: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; - case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.LABEL: docColor = docColor || Colors.LIGHT_GRAY; break; + case DocumentType.BUTTON: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.LINK: docColor = (isAnchor ? docColor : '') || 'transparent'; break; case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: case DocumentType.MAP: case DocumentType.SCREENSHOT: - case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.VID: docColor = docColor || (Colors.LIGHT_GRAY); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; docColor = docColor || (Doc.IsSystem(doc) - ? SettingsManager.Instance.userBackgroundColor + ? SettingsManager.userBackgroundColor : doc.annotationOn ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup ? undefined : doc._type_collection === CollectionViewType.Stacking ? - (darkScheme() ? Colors.MEDIUM_GRAY : Colors.DARK_GRAY) - : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : Colors.MEDIUM_GRAY)); + (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)"; - default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); + default: docColor = docColor || (Colors.WHITE); } return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor; } @@ -238,12 +238,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps switch (doc?.type) { case DocumentType.COL: return StrCast( - doc?.layout_borderRounding, + doc?.layout_boxShadow, doc?._type_collection === CollectionViewType.Pile ? '4px 4px 10px 2px' : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) - : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_borderRounding, '0.2vw 0.2vw 0.8vw')}` + : `${Colors.MEDIUM_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` ); case DocumentType.LABEL: @@ -261,14 +261,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } } case StyleProp.PointerEvents: - const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.(); - if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all'; + if (DocumentView.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all'; if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; - if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : '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: const lock = () => { @@ -281,22 +280,56 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } }; const filter = () => { + const dashView = DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard); const showFilterIcon = StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length - ? '#18c718bd' //'hasFilter' + ? 'green' // #18c718bd' //'hasFilter' : docProps?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.childFiltersByRanges().length ? 'orange' //'inheritsFilter' : undefined; return !showFilterIcon ? null : ( - <div className="styleProvider-filter" onClick={action(() => (SettingsManager.propertiesWidth = 250))}> - <FontAwesomeIcon icon={'filter'} size="lg" style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: showFilterIcon, zIndex: 1 }} /> + <div className="styleProvider-filter"> + <Dropdown + type={Type.TERT} + dropdownType={DropdownType.CLICK} + fillWidth + iconProvider={(active:boolean) => <div className='styleProvider-filterShift'><FaFilter/></div>} + closeOnSelect={true} + setSelectedVal={ + action((dv) => { + (dv as any).select(false); + (SettingsManager.propertiesWidth = 250); + setTimeout(action(() => { + if (PropertiesView.Instance) { + PropertiesView.Instance.CloseAll(); + PropertiesView.Instance.openFilters = true; + } + })); + }) + } + size={Size.XSMALL} + width={15} + height={15} + title={showFilterIcon === 'green' ? + "This view is filtered. Click to view/change filters": + "this view inherits filters from one of its parents"} + color={SettingsManager.userColor} + background={showFilterIcon} + items={[ ...(dashView ? [dashView]: []), ...(props?.docViewPath?.()??[]), ...(props?.DocumentView?[props?.DocumentView?.()]:[])] + .filter(dv => StrListCast(dv.rootDoc.childFilters).length || StrListCast(dv.rootDoc.childRangeFilters).length) + .map(dv => ({ + text: StrCast(dv.rootDoc.title), + val: dv as any, + style: {color:SettingsManager.userColor, background:SettingsManager.userBackgroundColor}, + } as IListItemProps)) } + /> </div> ); }; const audio = () => { const audioAnnoState = (doc: Doc) => StrCast(doc.audioAnnoState, 'stopped'); const audioAnnosCount = (doc: Doc) => StrListCast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations']).length; - if (!doc || props?.renderDepth === -1 || (!audioAnnosCount(doc) && audioAnnoState(doc) === 'stopped')) return null; + if (!doc || props?.renderDepth === -1 || !audioAnnosCount(doc)) return null; const audioIconColors: { [key: string]: string } = { recording: 'red', playing: 'green', stopped: 'blue' }; return ( <Tooltip title={<div>{StrListCast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations_text']).lastElement()}</div>}> @@ -317,7 +350,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) { - const color = StrCast(Doc.UserDoc().userColor); + const color = SettingsManager.userColor; return ( <IconButton size={Size.XSMALL} diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx index a551e5332..f07e38af1 100644 --- a/src/client/views/UndoStack.tsx +++ b/src/client/views/UndoStack.tsx @@ -1,13 +1,12 @@ -import { action, observable } from 'mobx'; +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 { UndoManager } from '../util/UndoManager'; -import './UndoStack.scss'; import { StrCast } from '../../fields/Types'; -import { Doc } from '../../fields/Doc'; -import { Popup, Type, isDark } from 'browndash-components'; -import { Colors } from './global/globalEnums'; import { SettingsManager } from '../util/SettingsManager'; +import { UndoManager } from '../util/UndoManager'; +import './UndoStack.scss'; interface UndoStackProps { width?: number; @@ -19,40 +18,51 @@ export class UndoStack extends React.Component<UndoStackProps> { @observable static HideInline: boolean; @observable static Expand: boolean; render() { - const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.Instance.userBackgroundColor; + const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor; + const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor; return this.props.inline && UndoStack.HideInline ? null : ( - <div className="undoStack-outerContainer"> - <Popup - text={'Undo/Redo Stack'} - color={UndoManager.batchCounter.get() ? 'yellow' : StrCast(Doc.UserDoc().userVariantColor)} - placement={`top-start`} - type={Type.TERT} - popup={ - <div - className="undoStack-commandsContainer" - ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} - style={{ - background: background, - color: isDark(background) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, - }}> - {UndoManager.undoStackNames.map((name, i) => ( - <div className="undoStack-resultContainer" key={i}> - <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div> - </div> - ))} - {Array.from(UndoManager.redoStackNames) - .reverse() - .map((name, i) => ( - <div className="undoStack-resultContainer" key={i}> - <div className="undoStack-commandString" style={{ fontWeight: 'bold', color: 'red' }}> - {name.replace(/[^\.]*\./, '')} + <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`} + type={Type.TERT} + popup={ + <div + className="undoStack-commandsContainer" + ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} + style={{ + background, + color, + }}> + {Array.from(UndoManager.undoStackNames).map((name, i) => ( + <div className="undoStack-resultContainer" key={i} + onClick={e => { + const size = UndoManager.undoStackNames.length; + for (let n = 0; n < size-i; n++ ) UndoManager.Undo(); } } + > + <div className="undoStack-commandString">{StrCast(name).replace(/[^\.]*\./, '')}</div> </div> - </div> - ))} - </div> - } - /> - </div> + ))} + {Array.from(UndoManager.redoStackNames) + .reverse() + .map((name, i) => ( + <div className="undoStack-resultContainer" key={i} onClick={e => + { 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(/[^\.]*\./, '')} + </div> + </div> + ))} + </div> + } + /> + </div> + </div> + </Tooltip> ); } } diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 6bd1d9f5f..8319f19ca 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -74,7 +74,10 @@ .carousel3DView-fwd, .carousel3DView-back { - top: 50%; + top: 0; + background: transparent; + width: calc((1 - #{$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%); + height: 100%; } .carousel3DView-fwd-scroll, @@ -108,8 +111,6 @@ opacity: 1; } -.carousel3DView-back:hover, -.carousel3DView-fwd:hover, .carousel3DView-back-scroll:hover, .carousel3DView-fwd-scroll:hover { background: lightgray; diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index d94e552b4..4bc72772a 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -2,14 +2,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { returnFalse, returnZero, Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } from '../global/globalCssVariables.scss'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView } from '../nodes/DocumentView'; import { StyleProp } from '../StyleProvider'; import './CollectionCarousel3DView.scss'; import { CollectionSubView } from './CollectionSubView'; @@ -46,6 +47,15 @@ export class CollectionCarousel3DView extends CollectionSubView() { .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this.props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2) .scale(1 / this.centerScale); + focus = (anchor: Doc, options: DocFocusOptions) => { + const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]); + if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return; + options.didMove = true; + const target = DocCast(anchor.annotationOn) ?? anchor; + const index = docs.indexOf(target); + index !== -1 && (this.layoutDoc._carousel_index = index); + return undefined; + }; @computed get content() { const currentIndex = NumCast(this.layoutDoc._carousel_index); const displayDoc = (childPair: { layout: Doc; data: Doc }) => { @@ -61,6 +71,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { LayoutTemplateString={this.props.childLayoutString} Document={childPair.layout} DataDoc={childPair.data} + focus={this.focus} ScreenToLocalTransform={this.childScreenToLocal} isContentActive={this.isChildContentActive} isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} @@ -85,8 +96,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; }; - onArrowClick = (e: React.MouseEvent, direction: number) => { - e.stopPropagation(); + onArrowClick = (direction: number) => { this.changeSlide(direction); !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = direction === 1 ? 'fwd' : 'back'); // while autoscroll is on, keep the other autoscroll button hidden !this.layoutDoc.autoScrollOn && this.fadeScrollButton(); // keep pause button visible while autoscroll is on @@ -117,16 +127,11 @@ export class CollectionCarousel3DView extends CollectionSubView() { }; @computed get buttons() { - if (!this.props.isContentActive()) return null; return ( <div className="arrow-buttons"> - <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}> - <FontAwesomeIcon icon="angle-left" size={'2x'} /> - </div> - <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}> - <FontAwesomeIcon icon="angle-right" size={'2x'} /> - </div> - {this.autoScrollButton} + <div title="click to go back" key="back" className="carousel3DView-back" onClick={e => this.onArrowClick(-1)} /> + <div title="click to advance" key="fwd" className="carousel3DView-fwd" onClick={e => this.onArrowClick(1)} /> + {/* {this.autoScrollButton} */} </div> ); } @@ -165,7 +170,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}> {this.content} </div> - {this.props.Document._chromeHidden ? null : this.buttons} + {this.buttons} <div className="dot-bar">{this.dots}</div> </div> ); diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 8660113cd..130b31325 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,8 +1,7 @@ - .collectionCarouselView-outer { - height : 100%; + height: 100%; .collectionCarouselView-caption { - height: 50; + height: 50; display: inline-block; width: 100%; } @@ -13,7 +12,8 @@ user-select: none; } } -.carouselView-back, .carouselView-fwd { +.carouselView-back, +.carouselView-fwd { position: absolute; display: flex; top: 42.5%; @@ -22,18 +22,19 @@ align-items: center; border-radius: 5px; justify-content: center; - color: rgba(255,255,255,0.5); - background : rgba(0,0,0, 0.1); + color: rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.1); &:hover { - color:white; + color: white; } } .carouselView-fwd { - right: 0; + right: 20; } .carouselView-back { - left: 0; + left: 20; } -.carouselView-back:hover, .carouselView-fwd:hover { +.carouselView-back:hover, +.carouselView-fwd:hover { background: lightgray; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index ea02bcd4c..040a584b8 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Opt } from '../../../fields/Doc'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, StopEvent } from '../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnZero, StopEvent } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; @@ -44,12 +44,14 @@ export class CollectionCarouselView extends CollectionSubView() { panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); + @computed get marginX() { + return NumCast(this.layoutDoc.caption_xMargin, 50); + } + captionWidth = () => this.props.PanelWidth() - 2 * this.marginX; @computed get content() { const index = NumCast(this.layoutDoc._carousel_index); const curDoc = this.childLayoutPairs?.[index]; - const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined }; - const marginX = NumCast(this.layoutDoc['caption_xMargin']); - const marginY = NumCast(this.layoutDoc['caption_yMargin']); + const captionProps = { ...this.props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; const show_captions = StrCast(this.layoutDoc._layout_showCaption); return !(curDoc?.layout instanceof Doc) ? null : ( <> @@ -58,6 +60,7 @@ export class CollectionCarouselView extends CollectionSubView() { {...this.props} NativeWidth={returnZero} NativeHeight={returnZero} + setContentView={undefined} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.props.isContentActive} @@ -79,9 +82,9 @@ export class CollectionCarouselView extends CollectionSubView() { style={{ display: show_captions ? undefined : 'none', borderRadius: this.props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding), - marginRight: marginX, - marginLeft: marginX, - width: `calc(100% - ${marginX * 2}px)`, + marginRight: this.marginX, + marginLeft: this.marginX, + width: `calc(100% - ${this.marginX * 2}px)`, }}> <FormattedTextBox key={index} {...captionProps} allowScroll={true} fieldKey={show_captions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} DataDoc={undefined} /> </div> diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index d93015506..c0530ab81 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,5 +1,285 @@ @import '../global/globalCssVariables.scss'; -@import '../../../../node_modules/golden-layout/src/css/goldenlayout-base.css'; + +.lm_root { + position: relative; +} +.lm_row > .lm_item { + float: left; +} +.lm_content { + overflow: hidden; + position: relative; +} +.lm_dragging, +.lm_dragging * { + cursor: move !important; + user-select: none; +} +.lm_maximised { + position: absolute; + top: 0; + left: 0; + z-index: 40; +} +.lm_maximise_placeholder { + display: none; +} +.lm_splitter { + position: relative; + z-index: 20; +} +.lm_splitter:hover, +.lm_splitter.lm_dragging { + background: orange; +} +.lm_splitter.lm_vertical .lm_drag_handle { + width: 100%; + position: absolute; + cursor: ns-resize; +} +.lm_splitter.lm_horizontal { + float: left; + height: 100%; +} +.lm_splitter.lm_horizontal .lm_drag_handle { + height: 100%; + position: absolute; + cursor: ew-resize; +} +.lm_header { + overflow: visible; + position: relative; + z-index: 1; +} +// .lm_header [class^='lm_'] { +// box-sizing: content-box !important; +// } +.lm_header .lm_controls { + position: absolute; + right: 3px; +} +.lm_header .lm_controls > li { + cursor: pointer; + float: left; + width: 18px; + height: 18px; + text-align: center; + top: 3px; +} +.lm_header ul { + margin: 0; + padding: 0; + list-style-type: none; +} +.lm_header .lm_tabs { + position: absolute; +} +.lm_header .lm_tab { + cursor: pointer; + float: left; + height: 25px; + padding: 0 10px 5px; + padding-right: 25px; + position: relative; + box-shadow: unset !important; +} +.lm_header .lm_tab i { + width: 2px; + height: 19px; + position: absolute; +} +.lm_header .lm_tab i.lm_left { + top: 0; + left: -2px; +} +.lm_header .lm_tab i.lm_right { + top: 0; + right: -2px; +} +.lm_header .lm_tab .lm_title { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; +} +.lm_header .lm_tab .lm_close_tab { + width: 14px; + height: 14px; + position: absolute; + top: 0; + right: 0; + text-align: center; +} +.lm_stack.lm_left .lm_header, +.lm_stack.lm_right .lm_header { + height: 100%; +} +.lm_dragProxy.lm_left .lm_header, +.lm_dragProxy.lm_right .lm_header, +.lm_stack.lm_left .lm_header, +.lm_stack.lm_right .lm_header { + width: 20px; + float: left; + vertical-align: top; +} +.lm_dragProxy.lm_left .lm_header .lm_tabs, +.lm_dragProxy.lm_right .lm_header .lm_tabs, +.lm_stack.lm_left .lm_header .lm_tabs, +.lm_stack.lm_right .lm_header .lm_tabs { + transform-origin: left top; + top: 0; + width: 1000px; +} +.lm_dragProxy.lm_left .lm_header .lm_controls, +.lm_dragProxy.lm_right .lm_header .lm_controls, +.lm_stack.lm_left .lm_header .lm_controls, +.lm_stack.lm_right .lm_header .lm_controls { + bottom: 0; +} +.lm_dragProxy.lm_left .lm_items, +.lm_dragProxy.lm_right .lm_items, +.lm_stack.lm_left .lm_items, +.lm_stack.lm_right .lm_items { + float: left; +} +.lm_dragProxy.lm_left .lm_header .lm_tabs, +.lm_stack.lm_left .lm_header .lm_tabs { + transform: rotate(-90deg) scaleX(-1); + left: 0; +} +.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab, +.lm_stack.lm_left .lm_header .lm_tabs .lm_tab { + transform: scaleX(-1); + margin-top: 1px; +} +.lm_dragProxy.lm_left .lm_header .lm_tabdropdown_list, +.lm_stack.lm_left .lm_header .lm_tabdropdown_list { + top: initial; + right: initial; + left: 20px; +} +.lm_dragProxy.lm_right .lm_content { + float: left; +} +.lm_dragProxy.lm_right .lm_header .lm_tabs, +.lm_stack.lm_right .lm_header .lm_tabs { + transform: rotate(90deg) scaleX(1); + left: 100%; + margin-left: 0; +} +.lm_dragProxy.lm_right .lm_header .lm_controls, +.lm_stack.lm_right .lm_header .lm_controls { + left: 3px; +} +.lm_dragProxy.lm_right .lm_header .lm_tabdropdown_list, +.lm_stack.lm_right .lm_header .lm_tabdropdown_list { + top: initial; + right: 20px; +} +.lm_dragProxy.lm_bottom .lm_header .lm_tab, +.lm_stack.lm_bottom .lm_header .lm_tab { + margin-top: 0; + border-top: none; +} +.lm_dragProxy.lm_bottom .lm_header .lm_controls, +.lm_stack.lm_bottom .lm_header .lm_controls { + top: 3px; +} +.lm_dragProxy.lm_bottom .lm_header .lm_tabdropdown_list, +.lm_stack.lm_bottom .lm_header .lm_tabdropdown_list { + top: initial; + bottom: 20px; +} +.lm_drop_tab_placeholder { + float: left; + width: 100px; + height: 10px; + visibility: hidden; +} +.lm_header .lm_controls .lm_tabdropdown:before { + content: ''; + width: 0; + height: 0; + vertical-align: middle; + display: inline-block; + border-top: 5px dashed; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + color: white; +} +.lm_header .lm_tabdropdown_list { + position: absolute; + top: 20px; + right: 0; + z-index: 5; + overflow: hidden; +} +.lm_header .lm_tabdropdown_list .lm_tab { + clear: both; + padding-right: 10px; + margin: 0; +} +.lm_header .lm_tabdropdown_list .lm_tab .lm_title { + width: 100px; +} +.lm_header .lm_tabdropdown_list .lm_close_tab { + display: none !important; +} +.lm_dragProxy { + position: absolute; + top: 0; + left: 0; + z-index: 30; +} +.lm_dragProxy .lm_header { + background: transparent; +} +.lm_dragProxy .lm_content { + border-top: none; + overflow: hidden; +} +.lm_dropTargetIndicator { + display: none; + position: absolute; + z-index: 20; +} +.lm_dropTargetIndicator .lm_inner { + width: 100%; + height: 100%; + position: relative; + top: 0; + left: 0; +} +.lm_transition_indicator { + display: none; + width: 20px; + height: 20px; + position: absolute; + top: 0; + left: 0; + z-index: 20; +} +.lm_popin { + width: 20px; + height: 20px; + position: absolute; + bottom: 0; + right: 0; + z-index: 9999; +} +.lm_popin > * { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} +.lm_popin > .lm_bg { + z-index: 10; +} +.lm_popin > .lm_icon { + z-index: 20; +} /*# sourceMappingURL=goldenlayout-base.css.map */ + @import '../../../../node_modules/golden-layout/src/css/goldenlayout-dark-theme.css'; .lm_title { @@ -35,6 +315,12 @@ width: max-content; height: 100%; display: flex; + max-width: 100; + text-overflow: ellipsis; +} + +.lm_active { + height: 27px !important; } .lm_active .lm_title { @@ -42,18 +328,33 @@ // font-weight: 700; } +.lm_header .lm_tabs { + overflow-y: hidden; + width: 100%; +} +ul.lm_tabs::before { + content: ' '; + position: absolute; + bottom: 0; + width: 100%; + z-index: 1; + pointer-events: none; + border: solid 1px black; +} .lm_header .lm_tab { // padding: 0px; // moved to MainView.scss, othwerise they get overridden by default stylings // opacity: 0.7; // box-shadow: none; // height: 25px; // border-bottom: black solid; + border-bottom: unset !important; + border-top-right-radius: 5px; + border-top-left-radius: 5px; .collectionDockingView-gear { display: none; } } - .lm_header .lm_tab.lm_active { padding: 0; opacity: 1; @@ -61,7 +362,11 @@ box-shadow: none; height: 27px; margin-right: 2px; - // border-bottom: unset; + z-index: 2 !important; + border-right: solid 2px; + border-left: solid 2px; + border-top: solid 2px; + border-color: black; .collectionDockingView-gear { display: inline-block; @@ -119,20 +424,55 @@ } .lm_close_tab { + display: inline-flex !important; padding: 0; + opacity: 1 !important; align-self: center; margin-right: 5px; - background-color: black; border-radius: 3px; - opacity: 1 !important; width: 15px !important; height: 15px !important; position: relative !important; - display: inline-flex !important; align-items: center; top: 0 !important; right: unset !important; left: 0 !important; + background-image: unset !important; + &::before { + content: '\a0x\a0'; + color: rgb(50, 50, 50); + margin: auto; + position: relative; + top: -2px; + } + &:hover { + &::before { + background: gray; + color: white; + } + } +} +.lm_close { + background-image: unset !important; + &:hover { + background: gray; + color: white !important; + } + &::before { + content: 'x'; + margin: auto; + position: relative; + top: -2; + font-size: medium; + font-family: sans-serif; + } +} + +.lm_iconWrap { + &:hover { + background: gray; + color: white !important; + } } .lm_tab, @@ -150,14 +490,6 @@ top: 0; left: 0; - // overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu) - .collectionDockingView-gear { - padding-left: 5px; - height: 15px; - width: 18px; - margin: auto; - } - .collectionDockingView-drag { touch-action: none; position: absolute; @@ -176,7 +508,6 @@ display: flex; align-content: center; justify-content: center; - background: $dark-gray; } .lm_controls > li { @@ -186,14 +517,38 @@ } .lm_controls .lm_popout { - transform: rotate(45deg); - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); + background-image: unset; + left: -3; + &:hover { + background: gray; + color: white !important; + } + } + li.lm_popout::before { + content: '+'; + margin: auto; + font-size: x-large; + top: -6; + position: relative; + } + .lm_maximise { + background-image: unset !important; + &::before { + content: '\25A3'; + margin: auto; + font-size: medium; + position: relative; + } + &:hover { + background: gray; + color: white !important; + } } .lm_maximised .lm_controls .lm_maximise { - opacity: 1; - transform: scale(0.8); - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) !important; + &::before { + content: '\25A2'; + } } .flexlayout__layout { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e15d57306..4873a61ff 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -3,16 +3,18 @@ import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; import * as GoldenLayout from '../../../client/goldenLayout'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { AclAdmin, AclEdit } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { GetEffectiveAcl, inheritParentAcls } from '../../../fields/util'; -import { emptyFunction, incrementTitleCopy } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -28,9 +30,8 @@ import './CollectionDockingView.scss'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; -import { DocumentManager } from '../../util/DocumentManager'; -import { AclAdmin, AclEdit } from '../../../fields/DocSymbols'; import React = require('react'); +import { SettingsManager } from '../../util/SettingsManager'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -60,7 +61,7 @@ export class CollectionDockingView extends CollectionSubView() { return this._goldenLayout._maximisedItem !== null; } private _goldenLayout: any = null; - + static _highlightStyleSheet: any = addStyleSheet(); constructor(props: SubCollectionViewProps) { super(props); if (this.props.renderDepth < 0) runInAction(() => (CollectionDockingView.Instance = this)); @@ -118,6 +119,7 @@ export class CollectionDockingView extends CollectionSubView() { const j = tab.header.parent.contentItems.indexOf(tab.contentItem); if (j !== -1) { tab.header.parent.contentItems[j].remove(); + CollectionDockingView.Instance.endUndoBatch(); return CollectionDockingView.Instance.layoutChanged(); } } @@ -329,6 +331,16 @@ export class CollectionDockingView extends CollectionSubView() { width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows { fireImmediately: true } ); + reaction( + () => [SettingsManager.userBackgroundColor, SettingsManager.userBackgroundColor], + () => { + clearStyleSheetRules(CollectionDockingView._highlightStyleSheet); + addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { background: `${SettingsManager.userBackgroundColor} !important` }); + addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { color: `${SettingsManager.userColor} !important` }); + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${SettingsManager.userBackgroundColor} !important` }); + }, + { fireImmediately: true } + ); } }; @@ -504,6 +516,23 @@ export class CollectionDockingView extends CollectionSubView() { } }); + let addNewDoc = action(() => { + const dashboard = Doc.ActiveDashboard; + if (dashboard) { + dashboard['pane-count'] = NumCast(dashboard['pane-count']) + 1; + const docToAdd = Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), + _height: this.props.PanelHeight(), + _layout_fitWidth: true, + _freeform_backgroundGrid: true, + title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, + }); + Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd); + inheritParentAcls(this.dataDoc, docToAdd, false); + CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); + } + }); + stack.header?.controlsContainer .find('.lm_close') //get the close icon .off('click') //unbind the current click handler @@ -523,31 +552,18 @@ export class CollectionDockingView extends CollectionSubView() { }) ); + stack.element.click((e: any) => { + if (stack.contentItems.length === 0 && Array.from(document.elementsFromPoint(e.originalEvent.x, e.originalEvent.y)).some(ele => ele?.className === 'empty-tabs-message')) { + addNewDoc(); + } + }); stack.header?.controlsContainer .find('.lm_maximise') //get the close icon .click(() => setTimeout(this.stateChanged)); stack.header?.controlsContainer .find('.lm_popout') //get the popout icon .off('click') //unbind the current click handler - .click( - action(() => { - // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size - const dashboard = Doc.ActiveDashboard; - if (dashboard) { - dashboard['pane-count'] = NumCast(dashboard['pane-count']) + 1; - const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), - _height: this.props.PanelHeight(), - _layout_fitWidth: true, - _freeform_backgroundGrid: true, - title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, - }); - Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd); - inheritParentAcls(this.dataDoc, docToAdd, false); - CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); - } - }) - ); + .click(addNewDoc); }; render() { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index f65e8698f..ec9d86c1a 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -41,6 +41,7 @@ import { COLLECTION_BORDER_WIDTH } from './CollectionView'; import { TabDocView } from './TabDocView'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionLinearView } from './collectionLinear'; +import { media_state } from '../nodes/AudioBox'; interface CollectionMenuProps { panelHeight: () => number; @@ -157,7 +158,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <Toggle toggleType={ToggleType.BUTTON} type={Type.PRIM} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} onClick={this.toggleTopBar} toggleStatus={SettingsManager.headerBarHeight > 0} icon={<FontAwesomeIcon icon={headerIcon} size="lg" />} @@ -166,7 +167,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <Toggle toggleType={ToggleType.BUTTON} type={Type.PRIM} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} onClick={this.toggleProperties} toggleStatus={SettingsManager.propertiesWidth > 0} icon={<FontAwesomeIcon icon={propIcon} size="lg" />} @@ -181,8 +182,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <div className="collectionMenu-container" style={{ - background: SettingsManager.Instance.userBackgroundColor, - // borderColor: StrCast(Doc.UserDoc().userColor) + background: SettingsManager.userBackgroundColor, + // borderColor: SettingsManager.userColor }}> {this.contMenuButtons} {hardCodedButtons} @@ -579,7 +580,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch @action startRecording = () => { - const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' }); + const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: media_state.PendingRecording }); CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }; diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 53a42d2a6..a69049b59 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -1,10 +1,10 @@ import React = require('react'); import { CursorProperty } from 'csstype'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Field, Opt } from '../../../fields/Doc'; import { DocData, Height, Width } from '../../../fields/DocSymbols'; -import { Id } from '../../../fields/FieldSymbols'; +import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; @@ -93,7 +93,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // we use availableWidth to convert from a percentage to a pixel count. @computed get availableWidth() { const numDividers = this.numGroupColumns - 1; - return this.maxColWidth - numDividers * this.DividerWidth; + return this.maxColWidth - numDividers * this.DividerWidth - 2 * NumCast(this.layoutDoc.xMargin); } // children is passed as a prop to the NoteTakingField, which uses this function @@ -216,7 +216,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { return this.props.styleProvider?.(doc, props, property); }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + isContentActive = () => this.props.isContentActive(); blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined); // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) @@ -521,6 +521,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { this.observer.observe(ref); } }} + PanelWidth={this.props.PanelWidth} select={this.props.select} addDocument={this.addDocument} chromeHidden={this.chromeHidden} @@ -589,6 +590,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { const rightHeader = this.colHeaderData[colIndex + 1]; leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); + const headers = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); + headers.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]()); }; // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view @@ -596,17 +599,15 @@ export class CollectionNoteTakingView extends CollectionSubView() { // allows the user to adjust the column widths. @computed get renderedSections() { TraceMobx(); - const entries = Array.from(this.Sections.entries()); - const sections = entries; - const eles: JSX.Element[] = []; - for (let i = 0; i < sections.length; i++) { - const col = this.sectionNoteTaking(sections[i][0], sections[i][1]); - eles.push(col); - if (i < sections.length - 1) { - eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); - } - } - return eles; + const sections = Array.from(this.Sections.entries()); + return sections.map((sec, i) => ( + <> + {this.sectionNoteTaking(sec[0], sec[1])} + {i === sections.length - 1 ? null : ( // + <CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} /> + )} + </> + )); } @computed get nativeWidth() { @@ -621,7 +622,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } @computed get backgroundEvents() { - return this.props.isContentActive() === false ? 'none' : undefined; + return this.isContentActive() === false ? 'none' : undefined; } observer: any; @@ -636,7 +637,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { style={{ overflowY: this.props.isContentActive() ? 'auto' : 'hidden', background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? 'all' : undefined, + pointerEvents: this.backgroundEvents, }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 3286d60bd..52cc21903 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -1,13 +1,13 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Copy, Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { Cast } from '../../../fields/Types'; +import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; @@ -50,6 +50,7 @@ interface CSVFieldColumnProps { maxColWidth: number; dividerWidth: number; availableWidth: number; + PanelWidth: () => number; } /** @@ -62,9 +63,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu // columnWidth returns the width of a column in absolute pixels @computed get columnWidth() { - if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return '100%'; + if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return `${(this.props.availableWidth / this.props.PanelWidth()) * 100}%`; const i = this.props.colHeaderData.indexOf(this.props.headingObject); - return this.props.colHeaderData[i].width * 100 + '%'; + return ((this.props.colHeaderData[i].width * this.props.availableWidth) / this.props.PanelWidth()) * 100 + '%'; } private dropDisposer?: DragManager.DragDropDisposer; @@ -297,6 +298,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu style={{ width: this.columnWidth, background: this._background, + marginLeft: this.props.headings().findIndex((h: any) => h[0] === this.props.headingObject) === 0 ? NumCast(this.props.Document.xMargin) : 0, }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx index a1309b11f..af822d917 100644 --- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx @@ -1,4 +1,5 @@ -import { action, observable } from 'mobx'; +import { action, observable, trace } from 'mobx'; +import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { UndoManager } from '../../util/UndoManager'; @@ -7,6 +8,7 @@ interface DividerProps { index: number; xMargin: number; setColumnStartXCoords: (movementX: number, colIndex: number) => void; + isContentActive: () => boolean | undefined; } /** @@ -14,24 +16,26 @@ interface DividerProps { * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns. */ +@observer export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> { @observable private isHoverActive = false; @observable private isResizingActive = false; @action private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { - const batch = UndoManager.StartBatch('resizing'); + let batch: UndoManager.Batch | undefined; setupMoveUpEvents( this, e, (e, down, delta) => { + if (!batch) batch = UndoManager.StartBatch('resizing'); this.props.setColumnStartXCoords(delta[0], this.props.index); return false; }, action(() => { this.isResizingActive = false; this.isHoverActive = false; - batch.end(); + batch?.end(); }), emptyFunction ); @@ -46,6 +50,7 @@ export class CollectionNoteTakingViewDivider extends React.Component<DividerProp display: 'flex', alignItems: 'center', cursor: 'col-resize', + pointerEvents: this.props.isContentActive() ? 'all' : 'none', }} onPointerEnter={action(() => (this.isHoverActive = true))} onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index bbd528e13..91701b213 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,20 +1,19 @@ -import { action, computed, IReactionDisposer, reaction } from 'mobx'; +import { action, computed, IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { Height, Width } from '../../../fields/DocSymbols'; +import { ScriptField } from '../../../fields/ScriptField'; import { NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { SelectionManager } from '../../util/SelectionManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; +import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import './CollectionPileView.scss'; import { CollectionSubView } from './CollectionSubView'; import React = require('react'); -import { ScriptField } from '../../../fields/ScriptField'; -import { OpenWhere } from '../nodes/DocumentView'; -import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'; @observer export class CollectionPileView extends CollectionSubView() { diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index de58e1fe7..ad3160a08 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,12 +2,12 @@ import React = require('react'); import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Doc, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { Cast, NumCast, ScriptCast } from '../../../fields/Types'; +import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; import { Docs } from '../../documents/Documents'; @@ -32,7 +32,7 @@ import './CollectionStackedTimeline.scss'; export type CollectionStackedTimelineProps = { Play: () => void; Pause: () => void; - playLink: (linkDoc: Doc) => void; + playLink: (linkDoc: Doc, options: DocFocusOptions) => void; playFrom: (seekTimeInSeconds: number, endTime?: number) => void; playing: () => boolean; setTime: (time: number) => void; @@ -161,6 +161,10 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this.layoutDoc.clipEnd = this.trimEnd; this._trimming = TrimScope.None; } + @action + public CancelTrimming() { + this._trimming = TrimScope.None; + } @action public setZoom(zoom: number) { @@ -677,7 +681,7 @@ interface StackedTimelineAnchorProps { height: number; toTimeline: (screen_delta: number, width: number) => number; styleProvider?: (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any; - playLink: (linkDoc: Doc) => void; + playLink: (linkDoc: Doc, options: DocFocusOptions) => void; setTime: (time: number) => void; startTag: string; endTag: string; @@ -793,7 +797,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) { const anchor = observable({ view: undefined as any }); const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => { - this.props.playLink(mark); + this.props.playLink(mark, options); return undefined; }; return { diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 255bc3889..dddb3ec71 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -395,16 +395,23 @@ } .collectionStackingView-addDocumentButton { - font-size: 75%; letter-spacing: 2px; cursor: pointer; + .editableView-container-editing { + text-align: right; + } .editableView-input { outline-color: black; letter-spacing: 2px; color: grey; border: 0px; + background: yellow; + text-align: right; padding-top: 10px; // : 12px 10px 11px 10px; + input { + text-align: right; + } } } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index e4a0d6dad..36f8bc101 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -31,6 +31,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow'; import './CollectionStackingView.scss'; import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn'; import { CollectionSubView } from './CollectionSubView'; +import { SettingsManager } from '../../util/SettingsManager'; const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { @@ -67,7 +68,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // it looks like this gets the column headers that Mehek was showing just now @computed get colHeaderData() { - return Cast(this.layoutDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); + return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); } // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { @@ -120,7 +121,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (this.colHeaderData === undefined) { // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out? // here we're making an empty list of column headers (again, what Mehek showed us) - this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(); + this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(); } } @@ -137,7 +138,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { margin: this.rootDoc._stacking_alignCenter ? 'auto' : undefined, width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> @@ -158,7 +159,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); if (this.colHeaderData === undefined) { - setTimeout(() => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0); + setTimeout(() => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0); return new Map<SchemaHeaderField, Doc[]>(); } const colHeaderData = Array.from(this.colHeaderData); @@ -207,7 +208,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // reset section headers when a new filter is inputted this._pivotFieldDisposer = reaction( () => this.pivotField, - () => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) + () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) ); this._layout_autoHeightDisposer = reaction( () => this.layoutDoc._layout_autoHeight, @@ -426,7 +427,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} - style={{ cursor: this._cursor, color: StrCast(Doc.UserDoc().userColor), left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}> + style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}> <FontAwesomeIcon icon={'arrows-alt-h'} /> </div> ); diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index ebb4ba5a1..3598d548a 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -22,9 +22,6 @@ import { EditableView } from '../EditableView'; import './CollectionStackingView.scss'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { Id } from '../../../fields/FieldSymbols'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; // So this is how we are storing a column interface CSVFieldColumnProps { @@ -215,15 +212,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const layoutItems: ContextMenuProps[] = []; const docItems: ContextMenuProps[] = []; const dataDoc = this.props.DataDoc || this.props.Document; - + const width = this._ele ? Number(getComputedStyle(this._ele).width.replace('px', '')) : 0; + const height = this._ele ? Number(getComputedStyle(this._ele).height.replace('px', '')) : 0; DocUtils.addDocumentCreatorMenuItems( doc => { FormattedTextBox.SelectOnLoad = doc[Id]; return this.props.addDocument?.(doc); }, this.props.addDocument, - x, - y, + 0, + 0, true ); @@ -275,8 +273,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC this.props.addDocument?.(created); } }); - const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(x, y, undefined, true); + const pt = this.props + .screenToLocalTransform() + .inverse() + .transformPoint(width - 30, height); + ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true); }; @computed get innards() { @@ -360,7 +361,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC //TODO: would be great if there was additional space beyond the frame, so that you can actually see your // bottom note //TODO: ok, so we are using a single column, and this is it! - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: 'calc(100% - 25px)', maxWidth: this.props.columnWidth / this.props.numGroupColumns - 25, marginBottom: 10 }}> + <div + key={`${heading}-add-document`} + onKeyDown={e => e.stopPropagation()} + className="collectionStackingView-addDocumentButton" + style={{ width: 'calc(100% - 25px)', maxWidth: this.props.columnWidth / this.props.numGroupColumns - 25, marginBottom: 10 }}> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index eb4685834..26272d2ee 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -210,11 +210,12 @@ export function CollectionSubView<X>(moreProps?: X) { const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop)); - if ((!dropAction || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) { + if ((!dropAction || dropAction === 'inSame' || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { - const canAdd = de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc); + const canAdd = + (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc)) && (dropAction !== 'inSame' || docDragData.draggedDocuments.every(d => d.embedContainer === this.rootDoc)); const moved = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse); added = canAdd || moved ? moved : undefined; } else { @@ -298,7 +299,7 @@ export function CollectionSubView<X>(moreProps?: X) { let source = split; if (split.startsWith('data:image') && split.includes('base64')) { const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [split] }); - if (accessPaths.agnostic.client.indexOf("dashblobstore") === -1) { + if (accessPaths.agnostic.client.indexOf('dashblobstore') === -1) { source = Utils.prepend(accessPaths.agnostic.client); } else { source = accessPaths.agnostic.client; @@ -347,10 +348,10 @@ export function CollectionSubView<X>(moreProps?: X) { } if (uriList || text) { - if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed')) { + if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed') || text.includes('www.youtube.com/shorts')) { const batch = UndoManager.StartBatch('youtube upload'); const generatedDocuments: Doc[] = []; - this.slowLoadDocuments((uriList || text).split('v=')[1].split('&')[0], options, generatedDocuments, text, completed, addDocument).then(batch.end); + this.slowLoadDocuments((uriList || text).split('v=').lastElement().split('&')[0].split('shorts/').lastElement(), options, generatedDocuments, text, completed, addDocument).then(batch.end); return; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index eed04b3ee..9e5ac77d9 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -149,7 +149,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if (isAlreadyInTree() !== sameTree) { console.log('WHAAAT'); } - dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree ? 'same' : dragData.dropAction; + dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree && dragData.dropAction !== 'inSame' ? 'same' : dragData.dropAction; e.stopPropagation(); } }; @@ -438,7 +438,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1) || 1; + const scale = this.props.NativeDimScaling?.() || 1; return ( <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}> {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeView_HasOverlay ? ( diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 88f892efc..ce19b3f9b 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,7 +1,7 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { ObjectField } from '../../../fields/ObjectField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -40,7 +40,7 @@ interface CollectionViewProps_ extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) layoutEngine?: () => string; - setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void; setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => void; ignoreUnrendered?: boolean; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index f379d09ff..26aa5a121 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { Popup, Type } from 'browndash-components'; import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -10,20 +10,20 @@ import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; -import { Colors, Shadows } from '../global/globalEnums'; +import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; -import { MainView } from '../MainView'; import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { DashFieldView } from '../nodes/formattedText/DashFieldView'; import { KeyValueBox } from '../nodes/KeyValueBox'; @@ -34,7 +34,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import React = require('react'); -import { Popup, Toggle, Type } from 'browndash-components'; const _global = (window /* browser */ || global) /* node */ as any; interface TabDocViewProps { @@ -133,6 +132,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { if (tab.element[0].children[1].children.length === 1) { iconWrap.className = 'lm_iconWrap lm_moreInfo'; + iconWrap.title = 'click for menu, drag to embed in document'; const dragBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -217,15 +217,15 @@ export class TabDocView extends React.Component<TabDocViewProps> { ); // highlight the tab when the tab document is brushed in any part of the UI - tab._disposers.reactionDisposer = reaction( - () => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), - ({ title, degree }) => { - //titleEle.value = title; - // titleEle.style.padding = degree ? 0 : 2; - // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; - }, - { fireImmediately: true } - ); + // tab._disposers.reactionDisposer = reaction( + // () => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), + // ({ title, degree }) => { + // titleEle.value = title; + // titleEle.style.padding = degree ? 0 : 2; + // titleEle.style.border = `${['gray', 'gray', 'gray'][degree]} ${['none', 'dashed', 'solid'][degree]} 2px`; + // }, + // { fireImmediately: true } + // ); // clean up the tab when it is closed tab.closeElement @@ -280,10 +280,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { if (pinProps.pinViewport) PresBox.pinDocView(pinDoc, pinProps, anchorDoc ?? doc); if (!pinProps?.audioRange && duration !== undefined) { - pinDoc.mediaStart = 'manual'; - pinDoc.mediaStop = 'manual'; - pinDoc.config_clipStart = NumCast(doc.clipStart); - pinDoc.config_clipEnd = NumCast(doc.clipEnd, duration); + pinDoc.presentation_mediaStart = 'manual'; + pinDoc.presentation_mediaStop = 'manual'; } if (pinProps?.activeFrame !== undefined) { pinDoc.config_activeFrame = pinProps?.activeFrame; @@ -352,7 +350,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { private onActiveContentItemChanged(contentItem: any) { if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) { this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab; - if (!this._view) setTimeout(() => SelectionManager.SelectView(this._view, false)); + if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== 'dontSelectOnActivate') setTimeout(() => SelectionManager.SelectView(this._view, false)); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } } @@ -384,7 +382,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { return LightboxView.AddDocTab(doc, location); case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, undefined, keyValue); - case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, undefined, keyValue); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, "dontSelectOnActivate", keyValue); case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue); } }; @@ -423,9 +421,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelHeight = () => this._panelHeight; miniMapColor = () => Colors.MEDIUM_GRAY; tabView = () => this._view; - disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform; + disableMinimap = () => !this._document; whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive); isContentActive = () => this._isContentActive; + waitForDoubleClick = () => (DocumentView.ExploreMode ? 'never' : undefined); @computed get docView() { return !this._activated || !this._document ? null : ( <> @@ -441,8 +440,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { hideTitle={this.props.keyValue} Document={this._document} DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} - onBrowseClick={MainView.Instance.exploreMode} - waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick} + onBrowseClick={DocumentView.exploreMode} + waitForDoubleClickToClick={this.waitForDoubleClick} isContentActive={this.isContentActive} isDocumentActive={returnFalse} PanelWidth={this.PanelWidth} @@ -463,9 +462,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { bringToFront={emptyFunction} pinToPres={TabDocView.PinDoc} /> - {this.disableMinimap() || this._document._type_collection !== CollectionViewType.Freeform ? null : ( - <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - )} + {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />} </> ); } @@ -477,7 +474,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { style={{ fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined, }} - onPointerEnter={action(() => (this._hovering = true))} + onPointerOver={action(() => (this._hovering = true))} onPointerLeave={action(() => (this._hovering = false))} onDragOver={action(() => (this._hovering = true))} onDragLeave={action(() => (this._hovering = false))} @@ -506,6 +503,18 @@ interface TabMinimapViewProps { PanelHeight: () => number; background: () => string; } +interface TabMiniThumbProps { + miniWidth: () => number; + miniHeight: () => number; + miniTop: () => number; + miniLeft: () => number; +} +@observer +class TabMiniThumb extends React.Component<TabMiniThumbProps> { + render() { + return <div className="miniThumb" style={{ width: `${this.props.miniWidth()}% `, height: `${this.props.miniHeight()}% `, left: `${this.props.miniLeft()}% `, top: `${this.props.miniTop()}% ` }} />; + } +} @observer export class TabMinimapView extends React.Component<TabMinimapViewProps> { static miniStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { @@ -517,25 +526,17 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { return 'none'; case StyleProp.DocContents: const background = ((type: DocumentType) => { + // prettier-ignore switch (type) { - 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'; + 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'; } })(doc.type as DocumentType); return !background ? undefined : <div style={{ width: doc[Width](), height: doc[Height](), position: 'absolute', display: 'block', background }} />; @@ -555,13 +556,13 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { returnMiniSize = () => NumCast(this.props.document._miniMapSize, 150); miniDown = (e: React.PointerEvent) => { const doc = this.props.document; - const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; 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; @@ -570,60 +571,57 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { emptyFunction ); }; - render() { - if (!this.renderBounds) return null; - const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; - const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; - const miniLeft = 50 + ((NumCast(this.props.document._freeform_panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; - const miniTop = 50 + ((NumCast(this.props.document._freeform_panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2; + 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-hidden"> - <Popup - icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} - color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)} - type={Type.TERT} - onPointerDown={e => e.stopPropagation()} - placement={'top-end'} - popup={ - <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> - <CollectionFreeFormView - Document={this.props.document} - docViewPath={returnEmptyDoclist} - 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 - setHeight={returnFalse} - isContentActive={emptyFunction} - isAnyChildContentActive={returnFalse} - select={emptyFunction} - isSelected={returnFalse} - dontRegisterView={true} - fieldKey={Doc.LayoutFieldKey(this.props.document)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - 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} - /> - <div className="miniOverlay" onPointerDown={this.miniDown}> - <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} /> - </div> - </div> - } + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> + <CollectionFreeFormView + Document={this.props.document} + docViewPath={returnEmptyDoclist} + 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 + setHeight={returnFalse} + isContentActive={emptyFunction} + isAnyChildContentActive={returnFalse} + select={emptyFunction} + isSelected={returnFalse} + dontRegisterView={true} + fieldKey={Doc.LayoutFieldKey(this.props.document)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + 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} /> + <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={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement={'top-end'} popup={this.popup} /> </div> ); } diff --git a/src/client/views/collections/TreeSort.ts b/src/client/views/collections/TreeSort.ts new file mode 100644 index 000000000..977acd030 --- /dev/null +++ b/src/client/views/collections/TreeSort.ts @@ -0,0 +1,6 @@ +export enum TreeSort { + AlphaUp = 'alphabetical from z', + AlphaDown = 'alphabetical from A', + Zindex = 'by Z index', + WhenAdded = 'when added', +} diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index d22e85880..cbcc7c710 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -21,8 +21,28 @@ } .treeView-bulletIcons { - width: 100%; + margin: auto; height: 100%; + // changes start here. + + .treeView-expandIcon { + display: none; + left: -8px; + position: absolute; + } + + .treeView-checkIcon { + left: 3.5px; + top: 2px; + position: absolute; + } + + &:hover { + .treeView-expandIcon { + display: unset; + } + } + // end changes position: relative; display: flex; flex-direction: row; @@ -44,6 +64,8 @@ position: relative; width: fit-content; min-height: 20px; + min-width: 15px; + margin-right: 3px; color: $medium-gray; border: #80808030 1px solid; border-radius: 5px; @@ -121,7 +143,6 @@ filter: opacity(0.2) !important; } } - //align-items: center; ::-webkit-scrollbar { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 3402a8c8d..f89aa065b 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../fields/Doc'; import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -35,6 +35,8 @@ import { CollectionView } from './CollectionView'; import './TreeView.scss'; import React = require('react'); import { IconButton, Size } from 'browndash-components'; +import { TreeSort } from './TreeSort'; +import { SettingsManager } from '../../util/SettingsManager'; export interface TreeViewProps { treeView: CollectionTreeView; @@ -77,12 +79,6 @@ const treeBulletWidth = function () { return Number(TREE_BULLET_WIDTH.replace('px', '')); }; -export enum TreeSort { - AlphaUp = 'alphabetical from z', - AlphaDown = 'alphabetical from A', - Zindex = 'by Z index', - WhenAdded = 'when added', -} /** * Renders a treeView of a collection of documents * @@ -227,6 +223,38 @@ export class TreeView extends React.Component<TreeViewProps> { } }; + @undoBatch + @action + recurToggle = (childList: Doc[]) => { + if (childList.length > 0) { + childList.forEach(child => { + child.runProcess = !!!child.runProcess; + TreeView.ToggleChildrenRun.get(child)?.(); + }); + } + }; + + @undoBatch + @action + getRunningChildren = (childList: Doc[]) => { + if (childList.length === 0) { + return []; + } + + const runningChildren: FieldResult[] = []; + childList.forEach(child => { + if (child.runProcess && TreeView.GetRunningChildren.get(child)) { + if (child.runProcess) { + runningChildren.push(child); + } + runningChildren.push(...(TreeView.GetRunningChildren.get(child)?.() ?? [])); + } + }); + return runningChildren; + }; + + static GetRunningChildren = new Map<Doc, any>(); + static ToggleChildrenRun = new Map<Doc, () => void>(); constructor(props: any) { super(props); if (!TreeView._openLevelScript) { @@ -235,6 +263,17 @@ export class TreeView extends React.Component<TreeViewProps> { } this._openScript = Doc.IsSystem(this.props.document) ? undefined : () => TreeView._openLevelScript!; this._editTitleScript = Doc.IsSystem(this.props.document) ? () => TreeView._openLevelScript! : () => TreeView._openTitleScript!; + + // set for child processing highligting + this.dataDoc.hasChildren = this.childDocs.length > 0; + // this.dataDoc.children = this.childDocs; + TreeView.ToggleChildrenRun.set(this.doc, () => { + this.recurToggle(this.childDocs); + }); + + TreeView.GetRunningChildren.set(this.doc, () => { + return this.getRunningChildren(this.childDocs); + }); } _treeEle: any; @@ -294,7 +333,7 @@ export class TreeView extends React.Component<TreeViewProps> { const pt = [e.clientX, e.clientY]; const rect = this._header.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length); + const inside = pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length); this._header.current!.className = 'treeView-header'; if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) { if (inside) this._header.current!.className += ' treeView-header-inside'; @@ -354,7 +393,7 @@ export class TreeView extends React.Component<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.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); + const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor(); const destDoc = this.doc; @@ -394,9 +433,9 @@ export class TreeView extends React.Component<TreeViewProps> { return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); }; const addDoc = inside ? localAdd : parentAddDoc; - const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same') && moveDocument; + const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument; const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeView_FreezeChildren).includes('add')) || forceAdd; - if (canAdd) { + if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this.props.parentTreeView?.doc))) { this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true); const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false); this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = false); @@ -559,7 +598,7 @@ export class TreeView extends React.Component<TreeViewProps> { } 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 && added && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); + !dataIsComputed && added && Doc.SetContainer(doc, this.doc); return added; }; @@ -578,7 +617,7 @@ export class TreeView extends React.Component<TreeViewProps> { return ( <div> {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : ( - <div className='treeView-sorting'> + <div className="treeView-sorting"> <IconButton color={sortings[sorting]?.color} size={Size.XSMALL} @@ -602,7 +641,7 @@ export class TreeView extends React.Component<TreeViewProps> { style={{ cursor: 'inherit' }} key={expandKey + 'more'} title={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`} - className='' //this.doc.treeView_HideTitle ? 'no-indent' : ''} + className="" //this.doc.treeView_HideTitle ? 'no-indent' : ''} onPointerDown={e => { downX = e.clientX; downY = e.clientY; @@ -698,7 +737,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { TraceMobx(); const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : !this.childDocs.length ? ':empty' : '')) || 'question'; - const color = StrCast(Doc.UserDoc().userColor); + const color = SettingsManager.userColor; const checked = this.onCheckedClick ? this.doc.treeView_Checked ?? 'unchecked' : undefined; return ( <div @@ -722,16 +761,15 @@ export class TreeView extends React.Component<TreeViewProps> { <IconButton color={color} icon={<FontAwesomeIcon icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} />} size={Size.XSMALL} /> ) ) : ( - <div className="treeView-bulletIcons" style={{ color: Doc.IsSystem(DocCast(this.doc.proto)) ? 'red' : undefined }}> - {this.onCheckedClick ? ( - <IconButton - color={color} - icon={<FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} />} - size={Size.XSMALL} + <div className="treeView-bulletIcons"> + <div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}> + <FontAwesomeIcon + size="sm" + style={{ display: this.childDocs?.length >= 1 ? 'block' : 'none' }} + icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} /> - ) : ( - <IconButton color={color} icon={<FontAwesomeIcon icon={iconType as IconProp} />} size={Size.XSMALL} /> - )} + </div> + {this.onCheckedClick ? null : typeof iconType === 'string' ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType} </div> )} </div> @@ -759,7 +797,7 @@ export class TreeView extends React.Component<TreeViewProps> { @observable headerEleWidth = 0; @computed get titleButtons() { const customHeaderButtons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations); - const color = StrCast(Doc.UserDoc().userColor); + const color = SettingsManager.userColor; return this.props.treeViewHideHeaderFields() || this.doc.treeView_HideHeaderFields ? null : ( <> {customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */} @@ -929,8 +967,8 @@ export class TreeView extends React.Component<TreeViewProps> { } })} Document={this.doc} + DataDoc={undefined} // or this.dataDoc? layout_fitWidth={returnTrue} - DataDoc={undefined} scriptContext={this} hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} @@ -977,8 +1015,8 @@ export class TreeView extends React.Component<TreeViewProps> { ref={this._tref} title="click to edit title. Double Click or Drag to Open" style={{ - backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? StrCast(Doc.UserDoc().userVariantColor) : undefined, - color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(StrCast(Doc.UserDoc().userVariantColor)) : undefined, + backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? SettingsManager.userVariantColor : undefined, + color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(SettingsManager.userVariantColor) : undefined, fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined, textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined, outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined, @@ -1007,7 +1045,7 @@ export class TreeView extends React.Component<TreeViewProps> { <div className="treeView-background" style={{ - background: StrCast(Doc.UserDoc().userColor), + background: SettingsManager.userColor, }} /> {contents} @@ -1103,7 +1141,7 @@ export class TreeView extends React.Component<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.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); + const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false)); }; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 1e76d373c..15b6e1d37 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -350,7 +350,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: groupNames.push({ type: 'text', text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: 'div', color: Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'black', x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: 'div', color: 'black', x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 89deb733a..24a758d8c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -228,6 +228,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetY); + const link = this.props.LinkDocs[0]; return { a, b, @@ -238,11 +239,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo textX, textY, // fully connected - pt1, - pt2, + // pt1, + // pt2, // this code adds space between links - // pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], - // pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13], + pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt1, + pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt2, }; } @@ -256,9 +257,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const linkColorList = Doc.UserDoc().link_ColorList as List<string>; const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>; const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); - const linkDescription = Field.toString(link.link_description as any as Field); + const linkDescription = Field.toString(link.link_description as any as Field).split('\n')[0]; - const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; + const linkSize = Doc.noviceMode || currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; //access stroke color using index of the relationship in the color list (default black) const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex]; @@ -268,15 +269,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; - if (link.link_displayArrow === undefined) { - link.link_displayArrow = false; - } - + const arrowScale = NumCast(link.link_displayArrow_scale, 3) return link.opacity === 0 || !a.width || !b.width || (!(Doc.UserDoc().showLinkLines || link.link_displayLine) && !aActive && !bActive) ? null : ( <> <defs> - <marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> - <polygon points="0 0, 3 1.5, 0 3" fill={stroke} /> + <marker id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4*arrowScale}`} markerHeight={`${3*arrowScale}`} refX="0" refY={`${1.5*arrowScale}`} orient="auto"> + <polygon points={`0 0, ${3*arrowScale} ${1.5*arrowScale}, 0 ${3*arrowScale}`} fill={stroke} /> </marker> <filter id="outline"> <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" /> @@ -306,7 +304,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo {textX === undefined || !linkDescription ? null : ( <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> <tspan> </tspan> - <tspan dy="2">{linkDescription}</tspan> + <tspan dy="2">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan> <tspan dy="2"> </tspan> </text> )} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f5cc1eb53..9df96fabc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -16,7 +16,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { aggregateBounds, emptyFunction, intersectRect, lightOrDark, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -33,7 +33,6 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariables.scss'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; -import { DocumentDecorations } from '../../DocumentDecorations'; import { GestureOverlay } from '../../GestureOverlay'; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; @@ -52,6 +51,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); +import { FollowLinkScript } from '../../../util/LinkFollower'; export type collectionFreeformViewProps = { NativeWidth?: () => number; @@ -97,7 +97,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _brushtimer: any; private _brushtimer1: any; - private get isAnnotationOverlay() { + public get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } public get scaleFieldKey() { @@ -155,7 +155,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] } : this.props.contentBounds?.() ?? aggregateBounds( - this._layoutElements.filter(e => e.bounds && e.bounds.width && !e.bounds.z).map(e => e.bounds!), + this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10) ); @@ -186,7 +186,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); } @computed get cachedGetTransform(): Transform { - return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); + return this.getContainerTransform() + .scale(this.props.isAnnotationOverlay ? 1 : 1 / this.nativeDim()) + .translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY) + .transform(this.cachedGetLocalTransform); } public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { @@ -311,6 +314,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection focus = (anchor: Doc, options: DocFocusOptions) => { if (this._lightboxDoc) return; + if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) return; 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.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); @@ -371,7 +375,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? (nd[1] / nd[0]) * NumCast(layoutDoc._width) : 300); - (d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront + !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront } if (this.layoutDoc._autoArrange || de.metaKey) { @@ -420,11 +424,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) { let added = false; // do nothing if link is dropped into any freeform view parent of dragged document - if (!linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc) { - const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' }); - added = this.props.addDocument?.(source) ? true : false; - de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed - } + const source = + !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc + ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' }) + : Docs.Create.FontIconDocument({ + title: 'anchor', + icon_label: '', + followLinkToggle: true, + icon: 'map-pin', + x: xp, + y: yp, + backgroundColor: '#ACCEF7', + layout_hideAllLinks: true, + layout_hideLinkButton: true, + _width: 15, + _height: 15, + _xPadding: 0, + onClick: FollowLinkScript(), + }); + added = this.props.addDocument?.(source) ? true : false; + de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed + e.stopPropagation(); !added && e.preventDefault(); return added; @@ -844,7 +864,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // create a new curve by appending all curves of the current segment together in order to render a single new stroke. if (!e.shiftKey) { this._eraserLock++; - this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment => + const segments = this.segmentInkStroke(intersect.inkView, intersect.t); + segments.forEach(segment => this.forceStrokeGesture( e, GestureUtils.Gestures.Stroke, @@ -1023,7 +1044,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action zoom = (pointX: number, pointY: number, deltaY: number): void => { - if (this.Document._isGroup || this.Document._freeform_noZoom) return; + if (this.Document._isGroup || this.Document[(this.props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); @@ -1031,12 +1052,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } - if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { + if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc[this.scaleFieldKey + '_min'])) { this.setPan(0, 0); return; } - if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { - deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale; + if (deltaScale * invTransform.Scale > NumCast(this.rootDoc[this.scaleFieldKey + '_max'], Number.MAX_VALUE)) { + deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_max'], 1) / invTransform.Scale; + } + if (deltaScale * invTransform.Scale < NumCast(this.rootDoc[this.scaleFieldKey + '_min'], this.isAnnotationOverlay ? 1 : 0)) { + deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_min'], 1) / invTransform.Scale; } const localTransform = invTransform.scaleAbout(deltaScale, x, y); @@ -1267,7 +1291,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @computed get _pointerEvents() { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = DocumentDecorations.Instance.Interacting + const pointerEvents = DocumentView.Interacting ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); return pointerEvents; @@ -1526,12 +1550,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); }); - if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) { - // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar - if (this.props.viewField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling()); - else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey])); - } - this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1885,6 +1903,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null); brushedView = () => this._brushedView; + gridColor = () => { + const backColor = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box'); + return lightOrDark(backColor); + }; @computed get marqueeView() { TraceMobx(); return ( @@ -1917,6 +1939,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection PanelHeight={this.props.PanelHeight} panX={this.panX} panY={this.panY} + color={this.gridColor} nativeDimScaling={this.nativeDim} zoomScaling={this.zoomScaling} layoutDoc={this.layoutDoc} @@ -2216,6 +2239,7 @@ interface CollectionFreeFormViewBackgroundGridProps { panY: () => number; PanelWidth: () => number; PanelHeight: () => number; + color: () => string; isAnnotationOverlay?: boolean; nativeDimScaling: () => number; zoomScaling: () => number; @@ -2237,7 +2261,7 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor const renderGridSpace = gridSpace * this.props.zoomScaling(); const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace; const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace; - const strokeStyle = Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'rgba(255,255,255,0.5)' : 'rgba(0, 0,0,0.5)'; + const strokeStyle = this.props.color(); return !this.props.nativeDimScaling() ? null : ( <canvas className="collectionFreeFormView-grid" @@ -2285,7 +2309,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY } while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview - ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime); + ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); Doc.linkFollowHighlight(dv?.props.Document, false); } }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 0f51fe6ff..607f9fb95 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -8,6 +8,7 @@ import { IconButton } from 'browndash-components'; import { StrCast } from '../../../../fields/Types'; import { Doc } from '../../../../fields/Doc'; import { computed } from 'mobx'; +import { SettingsManager } from '../../../util/SettingsManager'; @observer export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -27,43 +28,18 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { } @computed get userColor() { - return StrCast(Doc.UserDoc().userColor) + return SettingsManager.userColor; } render() { const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ width: 19 }} />; const buttons = ( <> - <IconButton - tooltip={"Create a Collection"} - onPointerDown={this.createCollection} - icon={<FontAwesomeIcon icon="object-group"/>} - color={this.userColor} - /> - <IconButton - tooltip={"Create a Grouping"} - onPointerDown={e => this.createCollection(e, true)} - icon={<FontAwesomeIcon icon="layer-group"/>} - color={this.userColor} - /> - <IconButton - tooltip={"Summarize Documents"} - onPointerDown={this.summarize} - icon={<FontAwesomeIcon icon="compress-arrows-alt"/>} - color={this.userColor} - /> - <IconButton - tooltip={"Delete Documents"} - onPointerDown={this.delete} - icon={<FontAwesomeIcon icon="trash-alt"/>} - color={this.userColor} - /> - <IconButton - tooltip={"Pin selected region"} - onPointerDown={this.pinWithView} - icon={presPinWithViewIcon} - color={this.userColor} - /> + <IconButton tooltip={'Create a Collection'} onPointerDown={this.createCollection} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} /> + <IconButton tooltip={'Create a Grouping'} onPointerDown={e => this.createCollection(e, true)} icon={<FontAwesomeIcon icon="layer-group" />} color={this.userColor} /> + <IconButton tooltip={'Summarize Documents'} onPointerDown={this.summarize} icon={<FontAwesomeIcon icon="compress-arrows-alt" />} color={this.userColor} /> + <IconButton tooltip={'Delete Documents'} onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} /> + <IconButton tooltip={'Pin selected region'} onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} /> </> ); return this.getElement(buttons); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index e0f5cbe5b..7c9d0f6e1 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -17,7 +17,6 @@ box-sizing: border-box; position: absolute; border-width: 1px; - border-color: black; pointer-events: none; .marquee-legend { bottom: -18px; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1c3da1dc5..4c502021d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -10,7 +10,7 @@ import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util'; -import { intersectRect, returnFalse, Utils } from '../../../../Utils'; +import { intersectRect, lightOrDark, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; @@ -18,7 +18,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; -import { OpenWhere } from '../../nodes/DocumentView'; +import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { PreviewCursor } from '../../PreviewCursor'; @@ -39,7 +39,7 @@ interface MarqueeViewProps { trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; - setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void; slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>; } @@ -197,18 +197,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const headers = ns[0].split('\t'); csvRows.push(headers.join(',')); ns[0] = ''; - const eachCell = ns.join('\t').split('\t') - let eachRow = [] - for (let i=1; i<eachCell.length; i++){ + const eachCell = ns.join('\t').split('\t'); + let eachRow = []; + for (let i = 1; i < eachCell.length; i++) { eachRow.push(eachCell[i].replace(/\,/g, '')); - if (i % headers.length == 0){ - csvRows.push(eachRow) + if (i % headers.length == 0) { + csvRows.push(eachRow); eachRow = []; } } - - const blob = new Blob([csvRows.join('\n')], {type: 'text/csv'}) - const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type:'text/csv'} + + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type: 'text/csv' }; const file = new File([blob], 'droppedTable', options); const loading = Docs.Create.LoadingDocument(file, options); DocUtils.uploadFileToDoc(file, {}, loading); @@ -225,7 +225,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode if (e.button === 2 || (e.button === 0 && e.altKey)) { // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true, false); + this.setPreviewCursor(e.clientX, e.clientY, true, false, this.props.Document); // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); // } @@ -298,17 +298,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } } - setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean) => { + setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => { if (hide) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Visible = false; + PreviewCursor.Doc = undefined; } else if (drag) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Visible = false; + PreviewCursor.Doc = undefined; this.cleanupInteractions(true); document.addEventListener('pointermove', this.onPointerMove, true); document.addEventListener('pointerup', this.onPointerUp, true); @@ -318,6 +320,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { + PreviewCursor.Doc = doc; PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } this.clearSelection(); @@ -332,7 +335,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; if (!this.props.trySelectCluster(e.shiftKey)) { - this.setPreviewCursor(e.clientX, e.clientY, false, false); + !DocumentView.ExploreMode && this.setPreviewCursor(e.clientX, e.clientY, false, false, this.props.Document); } else e.stopPropagation(); } } @@ -629,12 +632,20 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque transform: `translate(${p[0]}px, ${p[1]}px)`, width: Math.abs(v[0]), height: Math.abs(v[1]), + color: lightOrDark(this.props.Document?.backgroundColor ?? 'white'), + borderColor: lightOrDark(this.props.Document?.backgroundColor ?? 'white'), zIndex: 2000, }}> {' '} {this._lassoFreehand ? ( <svg height={2000} width={2000}> - <polyline points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" /> + <polyline // + points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} + fill="none" + stroke={lightOrDark(this.props.Document?.backgroundColor ?? 'white')} + strokeWidth="1" + strokeDasharray="3" + /> </svg> ) : ( <span className="marquee-legend" /> diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 6b3318bf3..d0c14a21d 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -17,7 +17,7 @@ .collectionLinearView-menuOpener { user-select: none; } - + > input:not(:checked) ~ &.true { background-color: transparent; } diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 2254b2e5f..faf7501c4 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,5 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; +import { Toggle, ToggleType, Type } from 'browndash-components'; import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -7,21 +8,20 @@ import { Doc, Opt } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; import { DocumentView } from '../../nodes/DocumentView'; import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; -import { StyleProp } from '../../StyleProvider'; import { UndoStack } from '../../UndoStack'; import { CollectionStackedTimeline } from '../CollectionStackedTimeline'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionLinearView.scss'; -import { Button, Toggle, ToggleType, Type } from 'browndash-components'; -import { Colors } from '../../global/globalEnums'; /** * CollectionLinearView is the class for rendering the horizontal collection @@ -145,6 +145,7 @@ export class CollectionLinearView extends CollectionSubView() { case '<LinkingUI>': return this.getLinkUI(); case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI(); case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />; + case '<Branching>': return Doc.UserDoc().isBranchingMode ? <BranchingTrailManager /> : null; } const nested = doc._type_collection === CollectionViewType.Linear; @@ -205,9 +206,10 @@ export class CollectionLinearView extends CollectionSubView() { const menuOpener = ( <Toggle text={Cast(this.props.Document.icon, 'string', null)} - icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />} + icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon color={SettingsManager.userColor} icon={isExpanded ? 'minus' : 'plus'} />} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} - color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)} onPointerDown={e => e.stopPropagation()} toggleType={ToggleType.BUTTON} toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)} @@ -225,7 +227,7 @@ export class CollectionLinearView extends CollectionSubView() { <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension(), pointerEvents: 'all' }}> { <> - {menuOpener} + {!this.layoutDoc.linearView_Expandable ? null : menuOpener} {!this.layoutDoc.linearView_IsOpen ? null : ( <div className="collectionLinearView-content" diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 80da4e1a2..81453c0b8 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,4 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@mui/material'; import { Button } from 'browndash-components'; import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; @@ -7,6 +8,7 @@ import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { returnFalse } from '../../../../Utils'; import { DragManager, dropActionType } from '../../../util/DragManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -301,13 +303,13 @@ export class CollectionMulticolumnView extends CollectionSubView() { .scale(this.props.NativeDimScaling?.() || 1); const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox); collector.push( - <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}> - {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={undoable(e => { - this.props.removeDocument?.(layout); - }, "close doc")} color={StrCast(Doc.UserDoc().userColor)} /> - <WidthLabel layout={layout} collectionDoc={Document} /> - </div>, + <Tooltip title={'Tab: ' + StrCast(layout.title)}> + <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}> + {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} + <Button tooltip="Remove document from header bar" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(e => this.props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} /> + <WidthLabel layout={layout} collectionDoc={Document} /> + </div> + </Tooltip>, <ResizeBar width={resizerWidth} key={'resizer' + i} diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 273e609ca..868b1140d 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -1,12 +1,12 @@ -import * as React from "react"; -import { observer } from "mobx-react"; -import { observable, action } from "mobx"; -import { Doc } from "../../../../fields/Doc"; -import { NumCast, StrCast } from "../../../../fields/Types"; -import { DimUnit } from "./CollectionMulticolumnView"; -import { UndoManager } from "../../../util/UndoManager"; -import { StyleProviderFunc } from "../../nodes/DocumentView"; -import { StyleProp } from "../../StyleProvider"; +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Doc } from '../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { DimUnit } from './CollectionMulticolumnView'; +import { UndoManager } from '../../../util/UndoManager'; +import { StyleProviderFunc } from '../../nodes/DocumentView'; +import { StyleProp } from '../../StyleProvider'; interface ResizerProps { width: number; @@ -31,13 +31,13 @@ export default class ResizeBar extends React.Component<ResizerProps> { this.props.select(false); e.stopPropagation(); e.preventDefault(); - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); + window.addEventListener('pointermove', this.onPointerMove); + window.addEventListener('pointerup', this.onPointerUp); this.isResizingActive = true; - this._resizeUndo = UndoManager.StartBatch("multcol resizing"); - } + this._resizeUndo = UndoManager.StartBatch('multcol resizing'); + }; private onPointerMove = ({ movementX }: PointerEvent) => { const { toLeft, toRight, columnUnitLength } = this.props; @@ -47,30 +47,30 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + const scale = StrCast(toNarrow._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1; toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementX) / scale); } if (toWiden) { - const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + const scale = StrCast(toWiden._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1; toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementX) / scale); } } - } + }; private get isActivated() { const { toLeft, toRight } = this.props; if (toLeft && toRight) { - if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel && StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toLeft._dimUnit, '*') === DimUnit.Pixel && StrCast(toRight._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; } else if (toLeft) { - if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toLeft._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; } else if (toRight) { - if (StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toRight._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; @@ -82,23 +82,25 @@ export default class ResizeBar extends React.Component<ResizerProps> { private onPointerUp = () => { this.isResizingActive = false; this.isHoverActive = false; - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); this._resizeUndo?.end(); this._resizeUndo = undefined; - } + }; render() { - return <div className="multiColumnResizer" - style={{ - width: this.props.width, - backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) - }} - onPointerEnter={action(() => this.isHoverActive = true)} - onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} - > - <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> - </div>; + return ( + <div + className="multiColumnResizer" + style={{ + pointerEvents: this.props.isContentActive?.() ? 'all' : 'none', + width: this.props.width, + backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor), + }} + onPointerEnter={action(() => (this.isHoverActive = true))} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}> + <div className={'multiColumnResizer-hdl'} onPointerDown={e => this.registerResizing(e)} /> + </div> + ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx index 006ef4df6..5a9d6a82c 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -1,12 +1,12 @@ -import * as React from "react"; -import { observer } from "mobx-react"; -import { observable, action } from "mobx"; -import { Doc } from "../../../../fields/Doc"; -import { NumCast, StrCast } from "../../../../fields/Types"; -import { DimUnit } from "./CollectionMultirowView"; -import { UndoManager } from "../../../util/UndoManager"; -import { StyleProp } from "../../StyleProvider"; -import { StyleProviderFunc } from "../../nodes/DocumentView"; +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Doc } from '../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { DimUnit } from './CollectionMultirowView'; +import { UndoManager } from '../../../util/UndoManager'; +import { StyleProp } from '../../StyleProvider'; +import { StyleProviderFunc } from '../../nodes/DocumentView'; interface ResizerProps { height: number; @@ -29,13 +29,13 @@ export default class ResizeBar extends React.Component<ResizerProps> { private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); e.preventDefault(); - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); + window.addEventListener('pointermove', this.onPointerMove); + window.addEventListener('pointerup', this.onPointerUp); this.isResizingActive = true; - this._resizeUndo = UndoManager.StartBatch("multcol resizing"); - } + this._resizeUndo = UndoManager.StartBatch('multcol resizing'); + }; private onPointerMove = ({ movementY }: PointerEvent) => { const { toTop: toTop, toBottom: toBottom, columnUnitLength } = this.props; @@ -45,30 +45,30 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + const scale = StrCast(toNarrow._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1; toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementY) / scale); } if (toWiden) { - const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + const scale = StrCast(toWiden._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1; toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementY) / scale); } } - } + }; private get isActivated() { const { toTop, toBottom } = this.props; if (toTop && toBottom) { - if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toTop._dimUnit, '*') === DimUnit.Pixel && StrCast(toBottom._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; } else if (toTop) { - if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toTop._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; } else if (toBottom) { - if (StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toBottom._dimUnit, '*') === DimUnit.Pixel) { return false; } return true; @@ -80,23 +80,25 @@ export default class ResizeBar extends React.Component<ResizerProps> { private onPointerUp = () => { this.isResizingActive = false; this.isHoverActive = false; - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); this._resizeUndo?.end(); this._resizeUndo = undefined; - } + }; render() { - return <div className="multiRowResizer" - style={{ - height: this.props.height, - backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) - }} - onPointerEnter={action(() => this.isHoverActive = true)} - onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} - > - <div className={"multiRowResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> - </div>; + return ( + <div + className="multiRowResizer" + style={{ + pointerEvents: this.props.isContentActive?.() ? 'all' : 'none', + height: this.props.height, + backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor), + }} + onPointerEnter={action(() => (this.isHoverActive = true))} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}> + <div className={'multiRowResizer-hdl'} onPointerDown={e => this.registerResizing(e)} /> + </div> + ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 52ebb7763..76bd392a5 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -9,12 +9,32 @@ .schema-table { background-color: $white; cursor: grab; + width: 100%; .schema-table-content { overflow: overlay; scroll-behavior: smooth; } + .schema-add { + position: relative; + height: 30; + display: flex; + align-items: center; + width: 100%; + text-align: right; + background: lightgray; + .editableView-container-editing { + width: 100%; + } + .editableView-input { + width: 100%; + float: right; + text-align: right; + background: yellow; + } + } + .schema-column-menu, .schema-filter-menu { background: $light-gray; @@ -173,6 +193,12 @@ flex-direction: row; height: 100%; overflow: auto; + + .schemaSelectionCell { + align-self: center; + width: 100%; + display: flex; + } } .schema-header-row > .schema-column-header:nth-child(2) > .left { @@ -185,7 +211,7 @@ overflow-x: hidden; overflow-y: auto; padding: 5px; - display: inline-block; + display: inline-flex; } .schema-row { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index babe5c810..d2b61167e 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -63,7 +63,7 @@ export class CollectionSchemaView extends CollectionSubView() { public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; - public static _newNodeInputHeight: number = 30; + public static _newNodeInputHeight: number = 20; public fieldInfos = new ObservableMap<string, FInfo>(); @observable _menuKeys: string[] = []; @@ -609,10 +609,10 @@ export class CollectionSchemaView extends CollectionSubView() { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); }; - getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field); + getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] == field); removeFieldFilters = (field: string) => { - this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove')); + this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(Doc.FilterSep)[1], 'remove')); }; onFilterKeyDown = (e: React.KeyboardEvent) => { @@ -766,8 +766,8 @@ export class CollectionSchemaView extends CollectionSubView() { return keyOptions.map(key => { let bool = false; if (filters !== undefined) { - const ind = filters.findIndex(filter => filter.split(':')[1] === key); - const fields = ind === -1 ? undefined : filters[ind].split(':'); + const ind = filters.findIndex(filter => filter.split(Doc.FilterSep)[1] === key); + const fields = ind === -1 ? undefined : filters[ind].split(Doc.FilterSep); bool = fields ? fields[2] === 'check' : false; } return ( @@ -836,6 +836,7 @@ export class CollectionSchemaView extends CollectionSubView() { <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div> <div className="schema-table" + style={{ width: `calc(100% - ${this.previewWidth + 4}px)` }} onWheel={e => this.props.isContentActive() && e.stopPropagation()} ref={r => { // prevent wheel events from passively propagating up through containers @@ -869,14 +870,18 @@ export class CollectionSchemaView extends CollectionSubView() { {this._columnMenuIndex !== undefined && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> - <EditableView - GetValue={returnEmptyString} - SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')} - placeholder={"Type ':' for commands"} - contents={'+ New Node'} - menuCallback={this.menuCallback} - height={CollectionSchemaView._newNodeInputHeight} - /> + {this.layoutDoc.chromeHidden ? null : ( + <div className="schema-add"> + <EditableView + GetValue={returnEmptyString} + SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')} + placeholder={"Type text to create note or ':' to create specific type"} + contents={'+ New Node'} + menuCallback={this.menuCallback} + height={CollectionSchemaView._newNodeInputHeight} + /> + </div> + )} </div> {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} {this.previewWidth > 0 && ( diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index e8e606030..4e418728f 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -16,6 +16,10 @@ import { CollectionSchemaView } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaTableCell } from './SchemaTableCell'; import { Transform } from '../../../util/Transform'; +import { IconButton, Size } from 'browndash-components'; +import { CgClose } from 'react-icons/cg'; +import { FaExternalLinkAlt } from 'react-icons/fa'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; @observer export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -110,22 +114,40 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { width: CollectionSchemaView._rowMenuWidth, pointerEvents: !this.props.isContentActive() ? 'none' : undefined, }}> - <div - className="schema-row-button" - onPointerDown={undoable(e => { - e.stopPropagation(); - this.props.removeDocument?.(this.rootDoc); - }, 'Delete Row')}> - <FontAwesomeIcon icon="times" /> - </div> - <div - className="schema-row-button" - onPointerDown={undoable(e => { - e.stopPropagation(); - this.props.addDocTab(this.rootDoc, OpenWhere.addRight); - }, 'Open Doc on Right')}> - <FontAwesomeIcon icon="external-link-alt" /> - </div> + <IconButton + tooltip="close" + icon={<CgClose size={'16px'} />} + size={Size.XSMALL} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(e => { + e.stopPropagation(); + this.props.removeDocument?.(this.rootDoc); + }, 'Delete Row') + ) + } + /> + <IconButton + tooltip="open preview" + icon={<FaExternalLinkAlt />} + size={Size.XSMALL} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(e => { + e.stopPropagation(); + this.props.addDocTab(this.rootDoc, OpenWhere.addRight); + }, 'Open schema Doc preview') + ) + } + /> </div> <div className="row-cells"> {this.schemaView?.columnKeys?.map((key, index) => ( diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 1c9c0de53..e18f27fb0 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -325,10 +325,34 @@ export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); const options = this.props.options?.map(facet => ({ value: facet, label: facet })); return ( - <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}> + <div className="schemaSelectionCell" style={{ color, textDecoration, cursor, pointerEvents }}> <div style={{ width: '100%' }}> <Select styles={{ + container: base => ({ + ...base, + height: 20, + }), + control: base => ({ + ...base, + height: 20, + minHeight: 20, + }), + placeholder: base => ({ + ...base, + top: '40%', + }), + input: base => ({ + ...base, + height: 20, + minHeight: 20, + margin: 0, + }), + indicatorsContainer: base => ({ + ...base, + height: 20, + transform: 'scale(0.75)', + }), menuPortal: base => ({ ...base, left: 0, diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 7b2ac5713..ddd99c836 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -71,7 +71,7 @@ $LEFT_MENU_WIDTH: 60px; $TREE_BULLET_WIDTH: 20px; $CAROUSEL3D_CENTER_SCALE: 1.3; -$CAROUSEL3D_SIDE_SCALE: 0.3; +$CAROUSEL3D_SIDE_SCALE: 0.6; $CAROUSEL3D_TOP: 15; :export { diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 38bf1042d..d8ed93bd7 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -85,10 +85,10 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'viewAllPersist', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'viewAllPersist', checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); // prettier-ignore - const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'viewAllPersist', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ + const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'viewAllPersist', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ ['grid', { checkResult: (doc:Doc) => BoolCast(doc._freeform_backgroundGrid, false), setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, @@ -101,6 +101,10 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snapli checkResult: (doc:Doc) => BoolCast(doc._freeform_fitContentsToBox, false), setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, }], + ['center', { + checkResult: (doc:Doc) => BoolCast(doc._stacking_alignCenter, false), + setDoc: (doc:Doc) => doc._stacking_alignCenter = !doc._stacking_alignCenter, + }], ['viewAllPersist', { checkResult: (doc:Doc) => false, setDoc: (doc:Doc) => doc.fitContentOnce = true @@ -182,8 +186,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]); // prettier-ignore const attrs:attrfuncs[] = [ - ['dictation', { checkResult: () => textView?._recording ? true:false, - toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }], + ['dictation', { checkResult: () => textView?._recordingDictation ? true:false, + toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }], ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 80cf93ed8..0b9f32eee 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -12,7 +12,6 @@ position: relative; border: 1px solid #e4e4e4; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - background: white; max-height: 230px; overflow-y: scroll; z-index: 10; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 65d13a6c3..9dc133e28 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -7,6 +7,7 @@ import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenu.scss'; import { LinkMenuGroup } from './LinkMenuGroup'; import React = require('react'); +import { SettingsManager } from '../../util/SettingsManager'; interface Props { docView: DocumentView; @@ -58,7 +59,7 @@ export class LinkMenu extends React.Component<Props> { const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds()); return ( - <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}> + <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style, background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div> </div> ); diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 8324a97d9..c1a5a634c 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -23,10 +23,10 @@ interface LinkMenuGroupProps { export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _menuRef = React.createRef<HTMLDivElement>(); - getBackgroundColor = (): string => { + getBackgroundColor = (): string | undefined => { const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); - let color = 'white'; + let color: string | undefined; // if this link's relationship property is not default "link", set its color if (link_relationshipList) { const relationshipIndex = link_relationshipList.indexOf(this.props.groupType); diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 806a2c381..e83f631a1 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -12,8 +12,6 @@ padding-top: 1px; padding-bottom: 1px; - background-color: white; - .linkMenu-name { position: relative; width: auto; @@ -25,7 +23,7 @@ .linkMenu-source-title { text-decoration: none; - color: rgb(43, 43, 43); + //color: rgb(43, 43, 43); font-size: 7px; padding-bottom: 0.75px; @@ -48,13 +46,12 @@ height: 12px; padding: 1px; margin-right: 3px; - color: rgb(161, 161, 161); + // color: rgb(161, 161, 161); } } .linkMenu-destination-title { text-decoration: none; - color: #4476f7; font-size: 13px; line-height: 0.9; padding-bottom: 2px; @@ -64,7 +61,6 @@ &:hover { text-decoration: underline; - color: rgb(60, 90, 156); //display: inline; text-overflow: break; cursor: pointer; @@ -75,7 +71,6 @@ .linkMenu-description { text-decoration: none; font-style: italic; - color: rgb(95, 97, 102); font-size: 9px; line-height: 0.9; margin-left: 20px; @@ -97,17 +92,7 @@ width: 100%; } - .link-metadata { - padding: 0 10px 0 16px; - margin-bottom: 4px; - color: $medium-gray; - font-style: italic; - font-size: 10.5px; - } - &:hover { - background-color: rgb(201, 239, 252); - .linkMenu-item-buttons { opacity: 1; } @@ -115,7 +100,6 @@ .linkMenu-item-content { .linkMenu-destination-title { text-decoration: underline; - color: rgb(60, 90, 156); //display: inline; text-overflow: break; } @@ -129,7 +113,7 @@ display: flex; opacity: 0; - .button { + .linkMenu-deleteButton { width: 20px; height: 20px; margin: 0; @@ -137,7 +121,7 @@ padding-right: 6px; border-radius: 50%; pointer-events: auto; - background-color: $dark-gray; + background-color: red; color: $white; font-size: 65%; transition: transform 0.2s; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index a81707ea4..bf2a4e1a9 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -69,10 +69,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } return this.props.sourceDoc; } - @action + onEdit = (e: React.PointerEvent) => { - LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc; - LinkManager.currentLinkAnchor = this.sourceAnchor; setupMoveUpEvents( this, e, @@ -90,8 +88,11 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight); } else { SelectionManager.SelectView(this.props.docView, false); + LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc; + LinkManager.currentLinkAnchor = LinkManager.currentLink ? this.sourceAnchor : undefined; + if ((SettingsManager.propertiesWidth ?? 0) < 100) { - SettingsManager.propertiesWidth = 250; + setTimeout(action(() => (SettingsManager.propertiesWidth = 250))); } } }) @@ -130,7 +131,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { }; deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); - + @observable _hover = false; render() { const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp; @@ -146,7 +147,15 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; return ( - <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}> + <div + className="linkMenu-item" + onPointerEnter={action(e => (this._hover = true))} + onPointerLeave={action(e => (this._hover = false))} + style={{ + fontSize: this._hover ? 'larger' : undefined, + fontWeight: this._hover ? 'bold' : undefined, + background: LinkManager.currentLink === this.props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + }}> <div className="linkMenu-item-content expand-two"> <div ref={this._drag} @@ -154,7 +163,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-item-buttons"> <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> - <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> + <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> </div> </Tooltip> @@ -188,12 +197,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} </p> </div> - {!this.props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description)}</p>} + {!this.props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description).split('\n')[0].substring(0, 50)}</p>} </div> <div className="linkMenu-item-buttons"> <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> - <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> + <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> </Tooltip> diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 6895c0746..9a9a89732 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -3,15 +3,14 @@ import { observer } from 'mobx-react'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../fields/Doc'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { SearchBox } from '../search/SearchBox'; import { DefaultStyleProvider } from '../StyleProvider'; import './LinkPopup.scss'; import React = require('react'); -import { OpenWhere } from '../nodes/DocumentView'; interface LinkPopupProps { linkFrom?: () => Doc | undefined; diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx index ff17e5c12..3a95e5f74 100644 --- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx @@ -1,14 +1,13 @@ -import './ButtonMenu.scss'; +import { action } from 'mobx'; import * as React from 'react'; -import { IButtonMenu } from './utils'; -import { NewLightboxView } from '../NewLightboxView'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { OpenWhereMod } from '../../nodes/DocumentView'; import { Doc } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; -import { MainView } from '../../MainView'; -import { action } from 'mobx'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { DocumentView, OpenWhereMod } from '../../nodes/DocumentView'; +import { NewLightboxView } from '../NewLightboxView'; +import './ButtonMenu.scss'; +import { IButtonMenu } from './utils'; export const ButtonMenu = (props: IButtonMenu) => { return ( @@ -40,10 +39,10 @@ export const ButtonMenu = (props: IButtonMenu) => { <div className="newLightboxView-exploreBtn" title="toggle explore mode to navigate among documents only" - style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }} + style={{ background: DocumentView.ExploreMode ? 'white' : undefined }} onClick={action(e => { e.stopPropagation(); - MainView.Instance._exploreMode = !MainView.Instance._exploreMode; + DocumentView.ExploreMode = !DocumentView.ExploreMode; })}></div> </div> ); diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx index 3acbd1a32..ca90f6a0f 100644 --- a/src/client/views/newlightbox/NewLightboxView.tsx +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -2,35 +2,32 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { GestureOverlay } from '../GestureOverlay'; -import { MainView } from '../MainView'; -import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; import { TabDocView } from '../collections/TabDocView'; +import { GestureOverlay } from '../GestureOverlay'; +import { LightboxView } from '../LightboxView'; import { DocumentView, OpenWhere } from '../nodes/DocumentView'; +import { DefaultStyleProvider } from '../StyleProvider'; +import { IRecommendation } from './components'; import { ExploreView } from './ExploreView'; -import { IBounds, emptyBounds } from './ExploreView/utils'; +import { emptyBounds, IBounds } from './ExploreView/utils'; import { NewLightboxHeader } from './Header'; import './NewLightboxView.scss'; import { RecommendationList } from './RecommendationList'; -import { IRecommendation } from './components'; -import { fetchKeywords, fetchRecommendations } from './utils'; -import { List } from '../../../fields/List'; -import { LightboxView } from '../LightboxView'; enum LightboxStatus { - RECOMMENDATIONS = "recommendations", - ANNOTATIONS = "annotations", - NONE = "none" + RECOMMENDATIONS = 'recommendations', + ANNOTATIONS = 'annotations', + NONE = 'none', } interface LightboxViewProps { @@ -63,30 +60,30 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { @observable private static _docView: Opt<DocumentView>; // keywords - @observable private static _keywords: string[] = [] + @observable private static _keywords: string[] = []; @action public static SetKeywords(kw: string[]) { - this._keywords = kw + this._keywords = kw; } @computed public static get Keywords() { - return this._keywords + return this._keywords; } // query - @observable private static _query: string = '' + @observable private static _query: string = ''; @action public static SetQuery(query: string) { - this._query = query + this._query = query; } @computed public static get Query() { - return this._query + return this._query; } // keywords - @observable private static _recs: IRecommendation[] = [] + @observable private static _recs: IRecommendation[] = []; @action public static SetRecs(recs: IRecommendation[]) { - this._recs = recs + this._recs = recs; } @computed public static get Recs() { - return this._recs + return this._recs; } // bounds @@ -108,7 +105,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { } // newLightbox sidebar status - @observable private static _sidebarStatus: Opt<string> = ""; + @observable private static _sidebarStatus: Opt<string> = ''; @action public static SetSidebarStatus(sidebarStatus: Opt<string>) { this._sidebarStatus = sidebarStatus; } @@ -129,7 +126,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { this._docFilters && (this._docFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; - MainView.Instance._exploreMode = false; + DocumentView.ExploreMode = false; } else { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); @@ -289,35 +286,38 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { @computed get documentView() { - if (!LightboxView.LightboxDoc) return null - else return (<GestureOverlay isActive={true}> - <DocumentView - ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))} - Document={LightboxView.LightboxDoc} - DataDoc={undefined} - PanelWidth={this.newLightboxWidth} - PanelHeight={this.newLightboxHeight} - LayoutTemplate={NewLightboxView.LightboxDocTemplate} - isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. - isContentActive={returnTrue} - styleProvider={DefaultStyleProvider} - ScreenToLocalTransform={this.newLightboxScreenToLocal} - renderDepth={0} - rootSelected={returnTrue} - docViewPath={returnEmptyDoclist} - childFilters={this.docFilters} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - addDocument={undefined} - removeDocument={undefined} - whenChildContentsActiveChanged={emptyFunction} - addDocTab={this.addDocTab} - pinToPres={TabDocView.PinDoc} - bringToFront={emptyFunction} - onBrowseClick={MainView.Instance.exploreMode} - focus={emptyFunction} - /> - </GestureOverlay>) + if (!LightboxView.LightboxDoc) return null; + else + return ( + <GestureOverlay isActive={true}> + <DocumentView + ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))} + Document={LightboxView.LightboxDoc} + DataDoc={undefined} + PanelWidth={this.newLightboxWidth} + PanelHeight={this.newLightboxHeight} + LayoutTemplate={NewLightboxView.LightboxDocTemplate} + isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. + isContentActive={returnTrue} + styleProvider={DefaultStyleProvider} + ScreenToLocalTransform={this.newLightboxScreenToLocal} + renderDepth={0} + rootSelected={returnTrue} + docViewPath={returnEmptyDoclist} + childFilters={this.docFilters} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + addDocument={undefined} + removeDocument={undefined} + whenChildContentsActiveChanged={emptyFunction} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + bringToFront={emptyFunction} + onBrowseClick={DocumentView.exploreMode} + focus={emptyFunction} + /> + </GestureOverlay> + ); } future = () => NewLightboxView._future; @@ -337,29 +337,28 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { NewLightboxView.SetNewLightboxDoc(undefined); } }}> - <div className={`app-document`} style={{gridTemplateColumns: `calc(100% - 400px) 400px`}}> - <div - className="newLightboxView-contents" - style={{ - top: 20, - left: 20, - width: this.newLightboxWidth(), - height: this.newLightboxHeight() - 40, - }}> + <div className={`app-document`} style={{ gridTemplateColumns: `calc(100% - 400px) 400px` }}> + <div + className="newLightboxView-contents" + style={{ + top: 20, + left: 20, + width: this.newLightboxWidth(), + height: this.newLightboxHeight() - 40, + }}> <NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} /> - {!NewLightboxView._explore ? - <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}> + {!NewLightboxView._explore ? ( + <div className="newLightboxView-doc" style={{ height: this.newLightboxHeight() }}> {this.documentView} </div> - : + ) : ( <div className={`explore`}> - <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/> + <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds} /> </div> - } - </div> - <RecommendationList keywords={NewLightboxView.Keywords}/> + )} + </div> + <RecommendationList keywords={NewLightboxView.Keywords} /> </div> - </div> ); } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 7c409c38c..8d80f1364 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,14 +1,15 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { DateField } from '../../../fields/DateField'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DateCast, NumCast } from '../../../fields/Types'; import { AudioField, nullAudio } from '../../../fields/URLField'; import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; import { LinkManager } from '../../util/LinkManager'; @@ -18,6 +19,7 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import './AudioBox.scss'; +import { DocFocusOptions } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; @@ -66,7 +68,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio _play: any = null; // timeout for playback - @observable _stackedTimeline: any; // CollectionStackedTimeline ref + @observable _stackedTimeline: CollectionStackedTimeline | null | undefined; // CollectionStackedTimeline ref @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; @@ -134,16 +136,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = - CollectionStackedTimeline.createAnchor( - this.rootDoc, - this.dataDoc, - this.annotationKey, - this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), - undefined, - undefined, - addAsAnnotation - ) || this.rootDoc; + const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); + const anchor = addAsAnnotation + ? CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.annotationKey, + this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), + undefined, + undefined, + addAsAnnotation + ) || this.rootDoc + : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc); return anchor; }; @@ -418,7 +423,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; // plays link - playLink = (link: Doc) => { + playLink = (link: Doc, options: DocFocusOptions) => { if (link.annotationOn === this.rootDoc) { if (!this.layoutDoc.dontAutoPlayFollowedLinks) { this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link)); @@ -473,8 +478,34 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; // for trim button, double click displays full clip, single displays curr trim bounds + onResetPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + this.timeline && + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, + action(e => { + if (this.timeline?.IsTrimming !== TrimScope.None) { + this.timeline?.CancelTrimming(); + } else { + this.beginEndtime = this.timeline?.trimEnd; + this.beginStarttime = this.timeline?.trimStart; + this.startTrim(TrimScope.All); + } + }) + ); + }; + + beginEndtime: number | undefined; + beginStarttime: number | undefined; + + // for trim button, double click displays full clip, single displays curr trim bounds onClipPointerDown = (e: React.PointerEvent) => { e.stopPropagation(); + this.beginEndtime = this.timeline?.trimEnd; + this.beginStarttime = this.timeline?.trimStart; this.timeline && setupMoveUpEvents( this, @@ -525,7 +556,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp r, (e, de) => { const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - de.complete.docDragData && this.timeline.internalDocDrop(e, de, de.complete.docDragData, xp); + de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp); }, this.layoutDoc, undefined @@ -587,9 +618,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp </div> {!this.miniPlayer && ( - <div className="audiobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish' : 'trim'} onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} /> - </div> + <> + <Tooltip title={<>trim audio</>}> + <div className="audiobox-button" onPointerDown={this.onClipPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} /> + </div> + </Tooltip> + {this.timeline?.IsTrimming == TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? ( + <></> + ) : ( + <Tooltip title={<>{this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}</>}> + <div className="audiobox-button" onPointerDown={this.onResetPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size={'1x'} /> + </div> + </Tooltip> + )} + </> )} </div> <div className="controls-right"> @@ -654,7 +698,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get renderTimeline() { return ( <CollectionStackedTimeline - ref={action((r: any) => (this._stackedTimeline = r))} + ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))} {...this.props} CollectionFreeFormDocumentView={undefined} dataFieldKey={this.fieldKey} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 6710cee63..4a438f826 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -40,7 +40,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'opacity', val: 1 }, { key: '_currentFrame' }, { key: 'freeform_scale', val: 1 }, - { key: 'freeform_scale', val: 1 }, { key: 'freeform_panX' }, { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index a12f1c12b..39c864b2b 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -60,6 +60,8 @@ opacity: 0.8; pointer-events: all; cursor: pointer; + width: 15px; + height: 15px; } .clear-button.before { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index a334e75f1..b09fcd882 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -109,12 +109,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; @undoBatch - clearDoc = (e: React.MouseEvent, fieldKey: string) => { - e.stopPropagation; // prevent click event action (slider movement) in registerSliding - delete this.dataDoc[fieldKey]; - }; + clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey]; + moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); addDoc = (doc: Doc, which: string) => { + if (this.dataDoc[which]) return false; this.dataDoc[which] = doc; return true; }; @@ -128,6 +127,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); + closeDown = (e: React.PointerEvent, which: string) => { + setupMoveUpEvents( + this, + e, + e => { + const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], '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); + return true; + }, + emptyFunction, + e => this.clearDoc(which) + ); + }; docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this.props.styleProvider?.(doc, props, property); @@ -136,13 +153,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl 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); + _closeRef = React.createRef<HTMLDivElement>(); render() { const clearButton = (which: string) => { return ( <div + ref={this._closeRef} className={`clear-button ${which}`} - onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding - onClick={e => this.clearDoc(e, which)}> + onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding + > <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> </div> ); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index a69881b7c..385ef5a1b 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -7,6 +7,9 @@ display: flex; flex-direction: row; } + .button-container { + pointer-events: unset; + } } .start-message { margin: 10px; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index f9f241234..8f32e2ba4 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -33,9 +33,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { static dataset = new ObservableMap<string, { [key: string]: string }[]>(); private _vizRenderer: LineChart | Histogram | PieChart | undefined; - // all CSV records in the dataset + // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { - return DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); + var records = DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); + return records?.filter(record => Object.keys(record).some(key => record[key])); } // currently chosen visualization type: line, pie, histogram, table @@ -57,11 +58,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes))); this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows); - this.layoutDoc.histogramBarColors = Field.Copy(data.histogramBarColors); - this.layoutDoc.defaultHistogramColor = data.defaultHistogramColor; - this.layoutDoc.pieSliceColors = Field.Copy(data.pieSliceColors); + 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 => { - if (key.startsWith('histogram_title') || key.startsWith('lineChart_title') || key.startsWith('pieChart_title')) { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { this.layoutDoc['_' + key] = data[key]; } }); @@ -84,11 +85,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { anchor.config_dataViz = this.dataVizView; anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; anchor.dataViz_selectedRows = Field.Copy(this.layoutDoc.dataViz_selectedRows); - anchor.histogramBarColors = Field.Copy(this.layoutDoc.histogramBarColors); - anchor.defaultHistogramColor = this.layoutDoc.defaultHistogramColor; - anchor.pieSliceColors = Field.Copy(this.layoutDoc.pieSliceColors); + 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 => { - if (key.startsWith('histogram_title') || key.startsWith('lineChart_title') || key.startsWith('pieChart_title')) { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { anchor[key] = this.layoutDoc[key]; } }); @@ -173,6 +174,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ) : ( <div className="dataViz" + style={{ + pointerEvents: this.props.isContentActive() === true ? "all" : "none" + }} onWheel={e => e.stopPropagation()} ref={r => r?.addEventListener( diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 35e5187b2..50dfe7f05 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -17,6 +17,18 @@ margin-top: -10px; margin-bottom: -10px; } + .asHistogram-checkBox { + // display: flex; + // flex-direction: row; + align-items: left; + align-self: left; + align-content: left; + justify-content: flex-end; + float: left; + left: 0; + position: relative; + margin-bottom: -35px; + } .selected-data{ align-items: center; text-align: center; @@ -76,9 +88,22 @@ fill: red; } } +.tableBox { + display: flex; + flex-direction: column; +} .table-container{ overflow: scroll; - margin: 10px; + margin: 5px; margin-left: 25px; - margin-top: 25px; + margin-right: 10px; + margin-bottom: 0; +} +.selectAll-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 5px; + margin-right: 10px; + float: right; }
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 50facf03e..e67e2bf31 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -383,7 +383,7 @@ export class Histogram extends React.Component<HistogramProps> { ) .attr('fill', d => { var barColor; - const barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + 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]; else { @@ -391,7 +391,7 @@ export class Histogram extends React.Component<HistogramProps> { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.defaultHistogramColor); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); }); }; @@ -399,28 +399,46 @@ export class Histogram extends React.Component<HistogramProps> { this.curBarSelected.attr('fill', color); const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - const barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec('string'), null); + 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)); barColors.push(StrCast(barName + '::' + color)); }; @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this.props.layoutDoc.defaultHistogramColor); + 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 barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec('string'), null); + 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; + if (svg) + svg.selectAll('rect').attr('fill', (d: any) => { + var 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]; + else { + const range = StrCast(each[0]).split(' to '); + if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; + } + }); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + }); + }; + render() { + this.updateBarColors(); this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.defaultHistogramColor) this.props.layoutDoc.defaultHistogramColor = '#69b3a2'; - if (!this.props.layoutDoc.histogramBarColors) this.props.layoutDoc.histogramBarColors = new List<string>(); + 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'; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); @@ -455,9 +473,9 @@ export class Histogram extends React.Component<HistogramProps> { tooltip={'Change Default Bar Color'} type={Type.SEC} icon={<FaFillDrip />} - selectedColor={StrCast(this.props.layoutDoc.defaultHistogramColor)} - setFinalColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} + 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')} size={Size.XSMALL} /> </div> diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 6c9922c0a..3de7a0c4a 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,21 +1,19 @@ +import { EditableText, Size } from 'browndash-components'; +import * as d3 from 'd3'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as d3 from 'd3'; import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; -import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; -import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; -import { EditableText, Size } from 'browndash-components'; -import { undoable } from '../../../../util/UndoManager'; export interface DataPoint { x: number; @@ -76,7 +74,7 @@ export class LineChart extends React.Component<LineChartProps> { // return selected x and y axes // otherwise, use the selection of whatever is linked to us const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; - const highlitedRowIds = NumListCast(incomingVizBox.rootDoc.dataViz_highlitedRows); + const highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.dataViz_highlitedRows); return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -91,10 +89,6 @@ export class LineChart extends React.Component<LineChartProps> { ({ dataSet, w, h }) => { if (dataSet) { this.drawChart([dataSet], this.rangeVals, w, h); - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); - this.incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); } }, { fireImmediately: true } @@ -106,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> { // could be blue colored to make it look like anchor // this.drawAnnotations() // loop through annotations and draw them - annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); + // annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); // this.drawAnnotations(annotations.x, annotations.y); }, { fireImmediately: true } @@ -156,10 +150,6 @@ export class LineChart extends React.Component<LineChartProps> { } }; - removeAnnotations(dataX: number, dataY: number) { - // loop through and remove any annotations that no longer exist - } - @action restoreView = (data: Doc) => { const coords = Cast(data.config_dataVizSelection, listSpec('number'), null); @@ -355,10 +345,9 @@ export class LineChart extends React.Component<LineChartProps> { } render() { - this.componentDidMount(); var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_lineChart_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_lineChart_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + 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'; if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index a8aa51897..561f39141 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -9,10 +9,10 @@ import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; -import { LinkManager } from '../../../../util/LinkManager'; import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import './Chart.scss'; +import { Checkbox } from '@material-ui/core'; export interface PieChartProps { rootDoc: Doc; @@ -52,13 +52,10 @@ export class PieChart extends React.Component<PieChartProps> { // organized by specified number percentages/ratios if one column is selected and it contains numbers // otherwise, assume data is organized by categories @computed get byCategory() { - if (this.props.axes.length === 1) { - return !/\d/.test(this.props.records[0][this.props.axes[0]]); - } - return true; + return !/\d/.test(this.props.records[0][this.props.axes[0]]) || this.props.layoutDoc.dataViz_pie_asHistogram; } // filters all data to just display selected data if brushed (created from an incoming link) - @computed get _piechartData() { + @computed get _pieChartData() { if (this.props.axes.length < 1) return []; const ax0 = this.props.axes[0]; @@ -90,7 +87,7 @@ export class PieChart extends React.Component<PieChartProps> { } componentDidMount = () => { this._disposers.chartData = reaction( - () => ({ dataSet: this._piechartData, w: this.width, h: this.height }), + () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }), ({ dataSet, w, h }) => { if (dataSet!.length > 0) { this.drawChart(dataSet, w, h); @@ -129,7 +126,7 @@ export class PieChart extends React.Component<PieChartProps> { : validData.map((d: { [x: string]: any }) => this.byCategory ? d[field] // - : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') + : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') ); }; @@ -162,7 +159,7 @@ export class PieChart extends React.Component<PieChartProps> { } if (lineCrossCount % 2 != 0) { // inside the slice of it crosses an odd number of edges - var showSelected = this.byCategory ? pieDataSet[index] : this._piechartData[index]; + var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; if (changeSelectedVariables) { // for when a bar is selected - not just hovered over sameAsCurrent = this._currSelected @@ -246,15 +243,17 @@ export class PieChart extends React.Component<PieChartProps> { // drawing the slices var selected = this.selectedData; var arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); - const sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); - const possibleDataPointVals = pieDataSet.map((each: { [x: string]: any | { valueOf(): number } }) => { + const possibleDataPointVals: { [x: string]: any }[] = []; + pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => { + var dataPointVal: { [x: string]: any } = {}; + dataPointVal[percentField] = each[percentField]; + if (descriptionField) dataPointVal[descriptionField] = each[descriptionField]; try { - each[percentField] = Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - } catch (error) { - //return each[percentField] == d.data; - } - return each; + dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '')); + } catch (error) {} + possibleDataPointVals.push(dataPointVal); }); + const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); arcs.append('path') .attr('fill', (d, i) => { var dataPoint; @@ -266,8 +265,9 @@ export class PieChart extends React.Component<PieChartProps> { } var sliceColor; if (dataPoint) { - var accessByName = dataPoint[this.props.axes[0]]; - sliceColors.forEach(each => each[0] == StrCast(accessByName) && (sliceColor = each[1])); + const sliceTitle = dataPoint[this.props.axes[0]]; + const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; + sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1])); } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) @@ -301,9 +301,9 @@ export class PieChart extends React.Component<PieChartProps> { .text(function (d) { var dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); - if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; + if (possibleDataPoints.length == 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])]; else { - dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; + dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])]; trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; } return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; @@ -312,25 +312,32 @@ export class PieChart extends React.Component<PieChartProps> { @action changeSelectedColor = (color: string) => { this.curSliceSelected.attr('fill', color); - var sliceName = this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); + const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - const sliceColors = Cast(this.props.layoutDoc.pieSliceColors, listSpec('string'), null); + const sliceColors = Cast(this.props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); sliceColors.map(each => { if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); }); sliceColors.push(StrCast(sliceName + '::' + color)); }; + @action changeHistogramCheckBox = () => { + this.props.layoutDoc.dataViz_pie_asHistogram = !this.props.layoutDoc.dataViz_pie_asHistogram; + this.drawChart(this._pieChartData, this.width, this.height); + }; + render() { var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_pieChart_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_pieChart_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.pieSliceColors) this.props.layoutDoc.pieSliceColors = new List<string>(); + if (!this.props.layoutDoc.dataViz_pie_sliceColors) this.props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { - curSelectedSliceName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const sliceTitle = this._currSelected[this.props.axes[0]]; + curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; selected = '{ '; Object.keys(this._currSelected).map(key => { key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; @@ -339,12 +346,12 @@ export class PieChart extends React.Component<PieChartProps> { selected += ' }'; } else selected = 'none'; var selectedSliceColor; - var sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); - sliceColors.map(each => { + var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + sliceColors.forEach(each => { if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; }); - if (this._piechartData.length > 0 || !this.parentViz) { + if (this._pieChartData.length > 0 || !this.parentViz) { return this.props.axes.length >= 1 ? ( <div className="chart-container"> <div className="graph-title"> @@ -359,6 +366,12 @@ export class PieChart extends React.Component<PieChartProps> { fillWidth /> </div> + {this.props.axes.length === 1 && /\d/.test(this.props.records[0][this.props.axes[0]]) ? ( + <div className={'asHistogram-checkBox'} style={{ width: this.props.width }}> + <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} /> + Organize data as histogram + </div> + ) : null} <div ref={this._piechartRef} /> {selected != 'none' ? ( <div className={'selected-data'}> diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 067dff07a..4d6027ca4 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,13 +1,13 @@ +import { Button, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, Field, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast } from '../../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils'; import { DragManager } from '../../../../util/DragManager'; -import { LinkManager } from '../../../../util/LinkManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; @@ -64,128 +64,149 @@ export class TableBox extends React.Component<TableBoxProps> { const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows); this.props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows); - this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data + this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data }; render() { if (this._tableData.length > 0) { return ( <div - className="table-container" - style={{ height: this.props.height }} - ref={r => - r?.addEventListener( - 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - (e: WheelEvent) => { - if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - }, - { passive: false } - ) - }> - <table className="table"> - <thead> - <tr className="table-row"> - {this.columns.map(col => ( - <th - key={this.columns.indexOf(col)} - style={{ - color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, - background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, - fontWeight: 'bolder', - border: '3px solid black', - }} - onPointerDown={e => { - const downX = e.clientX; - const downY = e.clientY; - setupMoveUpEvents( - {}, - e, - e => { - // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; - const targetCreator = (annotationOn: Doc | undefined) => { - const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); - embedding._dataViz = DataVizView.TABLE; - embedding._dataViz_axes = new List<string>([col, col]); - embedding._dataViz_parentViz = this.props.rootDoc; - embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; - embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); - embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; - embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); - return embedding; - }; - if (this.props.docView?.() && !Utils.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.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; - // e.annoDragData.linkSourceDoc.followLinkZoom = false; + className="tableBox" + tabIndex={0} + onKeyDown={e => { + if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { + e.stopPropagation(); + this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + } + }}> + <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'} /> + </div> + <div + className="table-container" + style={{ height: this.props.height }} + ref={r => + r?.addEventListener( + 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + (e: WheelEvent) => { + if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + }, + { passive: false } + ) + }> + <table className="table"> + <thead> + <tr className="table-row"> + {this.columns.map(col => ( + <th + key={this.columns.indexOf(col)} + style={{ + color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, + background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + fontWeight: 'bolder', + border: '3px solid black', + }} + onPointerDown={e => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + // dragging off a column to create a brushed DataVizBox + const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); + embedding._dataViz = DataVizView.TABLE; + embedding._dataViz_axes = new List<string>([col, col]); + embedding._dataViz_parentViz = this.props.rootDoc; + embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; + embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; + embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); + return embedding; + }; + if (this.props.docView?.() && !Utils.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; + // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + // e.annoDragData.linkSourceDoc.followLinkZoom = false; + } + }, } - }, - }); - return true; - } - return false; - }, - emptyFunction, - action(e => { - const newAxes = this.props.axes; - if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 1) newAxes[1] = col; - else newAxes.push(col); - this.props.selectAxes(newAxes); - }) - ); - }}> - {col} - </th> - ))} - </tr> - </thead> - <tbody> - {this._tableDataIds - ?.map(rowId => ({ record: this.props.records[rowId], rowId })) - .map(({ record, rowId }) => ( - <tr - key={rowId} - className="table-row" - onClick={action(e => { - const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); - if (e.metaKey) { - // highlighting a row - if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - else highlited?.push(rowId); - if (!selected?.includes(rowId)) selected?.push(rowId); - } else { - // selecting a row - if (selected?.includes(rowId)) { + ); + return true; + } + return false; + }, + emptyFunction, + action(e => { + const newAxes = this.props.axes; + if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); + else if (newAxes.length > 1) newAxes[1] = col; + else newAxes.push(col); + this.props.selectAxes(newAxes); + }) + ); + }}> + {col} + </th> + ))} + </tr> + </thead> + <tbody> + {this._tableDataIds + ?.map(rowId => ({ record: this.props.records[rowId], rowId })) + .map(({ record, rowId }) => ( + <tr + key={rowId} + className="table-row" + onClick={action(e => { + const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); + const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + if (e.metaKey) { + // highlighting a row if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); - selected.splice(selected.indexOf(rowId), 1); - } else selected?.push(rowId); - } - e.stopPropagation(); - })} - style={{ - background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', - width: '110%', - }}> - {this.columns.map(col => { - // each cell - const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; - return ( - <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> - {record[col]} - </td> - ); - })} - </tr> - ))} - </tbody> - </table> + else highlited?.push(rowId); + if (!selected?.includes(rowId)) selected?.push(rowId); + } else { + // 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); + } + e.stopPropagation(); + })} + style={{ + background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + width: '110%', + }}> + {this.columns.map(col => { + // each cell + const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; + return ( + <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> + {record[col]} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> </div> ); } else diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 2d8663c9c..8ac9d6804 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -47,6 +47,7 @@ import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import React = require('react'); import XRegExp = require('xregexp'); +import { MapPushpinBox } from './MapBox/MapPushpinBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -271,6 +272,7 @@ export class DocumentContentsView extends React.Component< PhysicsSimulationBox, SchemaRowBox, ImportElementBox, + MapPushpinBox, }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 6e2ed72b8..bccbd66e8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -9,6 +9,7 @@ import { action, observable } from 'mobx'; import { Id } from '../../../fields/FieldSymbols'; import { factory } from 'typescript'; import { LightboxView } from '../LightboxView'; +import { SettingsManager } from '../../util/SettingsManager'; @observer export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { @@ -29,6 +30,7 @@ export class DocumentIcon extends React.Component<{ view: DocumentView; index: n pointerEvents: 'all', opacity: this._hovered ? 0.3 : 1, position: 'absolute', + background: SettingsManager.userBackgroundColor, transform: `translate(${(left + right) / 2}px, ${top}px)`, }}> <Tooltip title={<>{this.props.view.rootDoc.title}</>}> diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 7723a088d..4db0bf5fa 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -18,10 +18,6 @@ import React = require('react'); import _ = require('lodash'); import { PinProps } from './trails'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - interface DocumentLinksButtonProps { View: DocumentView; Bottom?: boolean; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b72864567..ef96e64be 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate, Animation, DocData, Width } from '../../../fields/DocSymbols'; +import { AclPrivate, Animation, AudioPlay, DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -28,6 +28,7 @@ import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; @@ -51,7 +52,6 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { SettingsManager } from '../../util/SettingsManager'; const { Howl } = require('howler'); interface Window { @@ -100,6 +100,7 @@ export interface DocFocusOptions { effect?: Doc; // animation effect for focus noSelect?: boolean; // whether target should be selected after focusing playAudio?: boolean; // whether to play audio annotation on focus + playMedia?: boolean; // whether to play start target videos openLocation?: OpenWhere; // where to open a missing document zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections toggleTarget?: boolean; // whether to toggle target on and off @@ -120,6 +121,7 @@ export interface DocComponentView { reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views select?: (ctrlKey: boolean, shiftKey: boolean) => void; + focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>; menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. isAnyChildContentActive?: () => boolean; // is any child content of the document active onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected @@ -428,7 +430,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); if (this._doubleTap) { const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { @@ -629,7 +631,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); - if (linkdrag.linkSourceDoc) { + if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.rootDoc) { if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } @@ -700,11 +702,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const cm = ContextMenu.Instance; - if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled || DocumentView.ExploreMode) return; if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { - DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + if (this.rootDoc.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -744,8 +746,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' }); zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' }); zorderItems.push({ - description: this.rootDoc._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', - event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))), + description: !this.rootDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', + event: undoBatch(action(() => (this.rootDoc._keepZWhenDragged = !this.rootDoc._keepZWhenDragged))), icon: 'hand-point-up', }); !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' }); @@ -761,10 +763,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); - onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); + !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { - onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' }); + onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); } @@ -798,7 +800,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); } const constantItems: ContextMenuProps[] = []; - if (!Doc.IsSystem(this.rootDoc)) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking) { constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { @@ -965,6 +967,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps case StyleProp.ShowTitle: return ''; case StyleProp.PointerEvents: return 'none'; case StyleProp.Highlighting: return undefined; + case StyleProp.Opacity: { + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines); + return filtered.some(link => link._link_displayArrow) ? 0 : undefined; + } } return this.props.styleProvider?.(doc, props, property); }; @@ -1052,7 +1058,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => (dataDoc.audioAnnoState = 'recording')); + //runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); const stopFunc = () => { recorder.stop(); @@ -1069,16 +1075,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped'; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); - if (anno instanceof AudioField && audioAnnoState === 'stopped') { - new Howl({ - src: [anno.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')), - }); - this.dataDoc.audioAnnoState = 'playing'; + if (anno instanceof AudioField) { + switch (audioAnnoState) { + case 'stopped': + this.dataDoc[AudioPlay] = new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')), + }); + this.dataDoc.audioAnnoState = 'playing'; + break; + case 'playing': + this.dataDoc[AudioPlay]?.stop(); + this.dataDoc.audioAnnoState = 'stopped'; + break; + } } }; @@ -1114,7 +1128,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; - const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.Instance.userVariantColor)); + const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)); const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); const titleView = !showTitle ? null : ( <div @@ -1124,7 +1138,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button - color: lightOrDark(background), + color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> @@ -1138,14 +1152,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps display={'block'} fontSize={10} GetValue={() => { - this.props.select(false); return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { - if (this.props.layout_showTitle) { + if (this.rootDoc.layout_showTitle) { this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else { + } else if (!this.props.layout_showTitle) { Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; } } else { @@ -1276,7 +1289,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observer export class DocumentView extends React.Component<DocumentViewProps> { public static ROOT_DIV = 'documentView-effectsWrapper'; + @observable public static Interacting = false; @observable public static LongPress = false; + @observable public static ExploreMode = false; + @observable public static LastPressedSidebarBtn: Opt<Doc>; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach + @computed public static get exploreMode() { + return () => (DocumentView.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); + } @observable public docView: DocumentViewInternal | undefined | null; @observable public textHtmlOverlay: Opt<string>; @observable private _isHovering = false; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index d132707fa..14a3d16ef 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -6,11 +6,11 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { colorMapping } from '../../../../server/DashSession/Session/utilities/session_config'; -import { Utils } from '../../../../Utils'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { undoable, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; @@ -19,7 +19,6 @@ import { SelectedDocView } from '../../selectedDoc'; import { StyleProp } from '../../StyleProvider'; import { OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { RichTextMenu } from '../formattedText/RichTextMenu'; import './FontIconBox.scss'; import TrailsIcon from './TrailsIcon'; @@ -68,19 +67,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } }; - static GetShowLabels() { - return BoolCast(Doc.UserDoc()._showLabel); - } - static SetShowLabels(show: boolean) { - Doc.UserDoc()._showLabel = show; - } - static GetRecognizeGestures() { - return BoolCast(Doc.UserDoc()._recognizeGestures); - } - static SetRecognizeGestures(show: boolean) { - Doc.UserDoc()._recognizeGestures = show; - } - // Determining UI Specs @computed get label() { return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title)); @@ -140,11 +126,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); // Script for checking the outcome of the toggle const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); - const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; return ( <NumberDropdown color={color} + background={SettingsManager.userBackgroundColor} numberDropdownType={type} showPlusMinus={false} tooltip={this.label} @@ -168,14 +154,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <div className={`menuButton ${this.type} ${active}`} - style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} + style={{ color, backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => { this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; this.noTooltip = this.rootDoc.dropDownOpen; Doc.UnBrushAllDocs(); })}> {this.Icon(color)} - {!this.label || !FontIconBox.GetShowLabels() ? null : ( + {!this.label || !Doc.GetShowIconLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} {this.label}{' '} @@ -189,59 +175,69 @@ export class FontIconBox extends DocComponent<ButtonProps>() { ); } + dropdownItemDown = (e: React.PointerEvent, value: string | number) => { + setupMoveUpEvents( + this, + e, + (e: PointerEvent) => { + return ScriptCast(this.rootDoc.onDragScript)?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: { doc: value, e } }).result; + }, + emptyFunction, + emptyFunction + ); + return false; + }; + /** * Dropdown list */ @computed get dropdownListButton() { - const active: string = StrCast(this.rootDoc.dropDownOpen); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const script = ScriptCast(this.rootDoc.script); let noviceList: string[] = []; let text: string | undefined; - let dropdown = true; let getStyle: (val: string) => any = () => {}; let icon: IconProp = 'caret-down'; - let isViewDropdown: boolean = script?.script.originalScript.startsWith('setView'); - try { - if (isViewDropdown) { - const selectedDocs: Doc[] = SelectionManager.Docs(); - const selected = SelectionManager.Docs().lastElement(); - if (selected) { - if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._type_collection); + const isViewDropdown = script?.script.originalScript.startsWith('setView'); + if (isViewDropdown) { + const selected = SelectionManager.Docs(); + if (selected.lastElement()) { + if (StrCast(selected.lastElement().type) === DocumentType.COL) { + text = StrCast(selected.lastElement()._type_collection); + } else { + if (selected.length > 1) { + text = selected.length + ' selected'; } else { - dropdown = false; - if (selectedDocs.length > 1) { - text = selectedDocs.length + ' selected'; - } else { - text = Utils.cleanDocumentType(StrCast(selected.type) as DocumentType); - icon = Doc.toIcon(selected); - } - return <Popup icon={<FontAwesomeIcon size={'1x'} icon={icon} />} text={text} type={Type.TERT} color={color} popup={<SelectedDocView selectedDocs={selectedDocs} />} fillWidth />; + text = Utils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType); + icon = Doc.toIcon(selected.lastElement()); } - } else { - dropdown = false; - return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />; + return ( + <Popup + icon={<FontAwesomeIcon size={'1x'} icon={icon} />} + text={text} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + popup={<SelectedDocView selectedDocs={selected} />} + fillWidth + /> + ); } - noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; } else { - text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - getStyle = (val: string) => { - return { fontFamily: val }; - }; + return <Button text="None Selected" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} fillWidth inactive />; } - } catch (e) { - console.log(e); + noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Carousel3D, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; + } else { + text = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result; + // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + getStyle = (val: string) => ({ fontFamily: val }); } // Get items to place into the list const list: IListItemProps[] = this.buttonList .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) .map(value => ({ - text: value.charAt(0).toUpperCase() + value.slice(1), + text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title), val: value, style: getStyle(value), onClick: undoable(() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), value), @@ -251,10 +247,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <Dropdown selectedVal={text} - setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)} - color={color} - type={isViewDropdown ? Type.TERT : Type.PRIM} + setSelectedVal={undoable(value => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), `dropdown select ${this.label}`)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + type={Type.TERT} + closeOnSelect={false} dropdownType={DropdownType.SELECT} + onItemDown={this.dropdownItemDown} items={list} tooltip={this.label} fillWidth @@ -290,6 +289,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { selectedColor={curColor} type={Type.PRIM} color={color} + background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} tooltip={tooltip} label={this.label} @@ -310,6 +310,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { tooltip={`Toggle ${tooltip}`} type={Type.PRIM} color={color} + background={SettingsManager.userBackgroundColor} label={this.label} items={DocListCast(this.rootDoc.data).map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as any} color={color} />, @@ -344,6 +345,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} + //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} @@ -400,8 +402,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { case ButtonType.ToggleButton: return this.toggleButton; case ButtonType.ClickButton: case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; - case ButtonType.TextButton: return <Button {...btnProps} text={StrCast(this.rootDoc.buttonText)}/>; - case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + case ButtonType.TextButton: return <Button {...btnProps} color={color} + background={SettingsManager.userBackgroundColor} text={StrCast(this.rootDoc.buttonText)}/>; + case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} + background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } return this.defaultButton; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f5c6a9273..1ab120af5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -85,8 +85,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const visibleAnchor = this._getAnchor?.(this._savedAnnotations, false); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = - this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor + visibleAnchor ?? Docs.Create.ConfigDocument({ title: 'ImgAnchor:' + this.rootDoc.title, config_panX: NumCast(this.layoutDoc._freeform_panX), @@ -96,8 +97,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; - /* addAsAnnotation &&*/ this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc); + addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.rootDoc); return anchor; } return this.rootDoc; @@ -230,13 +231,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto['data_nativeWidth'] = anchw; croppingProto['data_nativeHeight'] = anchh; croppingProto.freeform_scale = viewScale; - croppingProto.freeform_scaleMin = viewScale; + croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_panX = anchx / viewScale; croppingProto.freeform_panY = anchy / viewScale; - croppingProto.freeform_panXMin = anchx / viewScale; - croppingProto.freeform_panXMax = anchw / viewScale; - croppingProto.freeform_panYMin = anchy / viewScale; - croppingProto.freeform_panYMax = anchh / viewScale; + croppingProto.freeform_panX_min = anchx / viewScale; + croppingProto.freeform_panX_max = anchw / viewScale; + croppingProto.freeform_panY_min = anchy / viewScale; + croppingProto.freeform_panY_max = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width](); @@ -502,7 +503,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._marqueeing = undefined; this.props.select(false); }; - focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options); + focus = (anchor: Doc, options: DocFocusOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); _ffref = React.createRef<CollectionFreeFormView>(); savedAnnotations = () => this._savedAnnotations; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index b0d041bdd..f22cb195f 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -111,7 +111,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { X </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === props.fieldKey)?.[1].description}> + <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === props.fieldKey)?.[1].description ?? ''}> <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}> {'('.repeat(parenCount)} {props.fieldKey} diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index c68e55f73..28216394d 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -1,7 +1,6 @@ .linkDocPreview { position: absolute; pointer-events: all; - background-color: lightblue; border: 8px solid white; border-radius: 7px; box-shadow: 3px 3px 1.5px grey; @@ -11,7 +10,6 @@ cursor: pointer; .linkDocPreview-inner { - background-color: white; width: 100%; height: 100%; pointer-events: none; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index d69009415..198cbe851 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; import { Doc, DocCastAsync, Opt } from '../../../fields/Doc'; -import { DirectLinks, Height, Width } from '../../../fields/DocSymbols'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; @@ -13,12 +13,13 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; +import { SearchUtil } from '../../util/SearchUtil'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; -import { SearchBox } from '../search/SearchBox'; import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -174,9 +175,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this.props.hrefs?.length) { const webDoc = - Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? + Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); - this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); + DocumentManager.Instance.showDocument(webDoc, { + openLocation: OpenWhere.lightbox, + willPan: true, + zoomTime: 500, + }); + //this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; @@ -198,7 +204,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( - <div className="linkDocPreview-info"> + <div className="linkDocPreview-info" style={{ background: SettingsManager.userBackgroundColor }}> <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}> <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> <div className="linkDocPreview-button" onPointerDown={this.editLink}> @@ -296,7 +302,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + style={{ borderColor: SettingsManager.userColor, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> {this.docPreview} </div> ); diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index d4a7e18f2..cabd4de05 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -5,34 +5,28 @@ justify-content: center; background-color: #fdfdfd; height: 100%; + width: 100%; align-items: center; - .textContainer, - .text { - overflow: hidden; + .loadingBox-textContainer, + .loadingBox-title { text-overflow: ellipsis; max-width: 80%; text-align: center; - display: flex; - flex-direction: column; + display: block; gap: 8px; align-items: center; + white-space: normal; + word-break: all; + .loadingBox-headerText { + text-align: center; + font-weight: bold; + height: auto; + width: 100%; + } + } + .loadingBox-spinner { + position: absolute; + top: 0; + left: 0; } -} - -.textContainer { - margin: 5px; -} - -.textContainer { - justify-content: center; - align-content: center; -} - -.headerText { - text-align: center; - font-weight: bold; -} - -.spinner { - text-align: center; } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index fcbd0128d..bdc074e0c 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,14 +1,14 @@ -import { action, observable, runInAction } from 'mobx'; +import { observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; import { Doc } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; import { StrCast } from '../../../fields/Types'; import { Networking } from '../../Network'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; -import { Id } from '../../../fields/FieldSymbols'; /** * LoadingBox Class represents a placeholder doc for documents that are currently @@ -44,9 +44,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.rootDoc.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { - const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. + const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. runInAction(() => (this.progress = result.progress)); - this._timer = setTimeout(updateFunc, 1000); + !this.rootDoc.loadingError && (this._timer = setTimeout(updateFunc, 1000)); }; this._timer = setTimeout(updateFunc, 1000); } @@ -58,10 +58,14 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { render() { return ( <div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}> - <div className="textContainer"> - <p className="headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p> - <span className="text">{StrCast(this.rootDoc.title)}</span> - {this.rootDoc.loadingError ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />} + <div className="loadingBox-textContainer"> + <span className="loadingBox-title">{StrCast(this.rootDoc.title)}</span> + <p className="loadingBox-headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p> + {this.rootDoc.loadingError ? null : ( + <div className="loadingBox-spinner"> + <ReactLoading type="spinningBubbles" color="blue" height={100} width={100} /> + </div> + )} </div> </div> ); diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss new file mode 100644 index 000000000..6990bdcf1 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -0,0 +1,54 @@ +.anchorMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; +} +.anchorMenu-highlighter { + padding-right: 5px; + .antimodeMenu-button { + padding: 0; + padding: 0; + padding-right: 0px; + padding-left: 0px; + width: 5px; + } +} +.anchor-color-preview-button { + width: 25px !important; + .anchor-color-preview { + display: flex; + flex-direction: column; + padding-right: 3px; + width: unset !important; + .color-preview { + width: 60%; + top: 80%; + height: 4px; + position: relative; + top: unset; + width: 15px; + margin-top: 5px; + display: block; + } + } +} + +.color-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + button.color-button { + width: 20px; + height: 20px; + border-radius: 15px !important; + margin: 3px; + border: 2px solid transparent !important; + padding: 3px; + + &.active { + border: 2px solid white; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx new file mode 100644 index 000000000..f0827936b --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -0,0 +1,143 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { ColorState } from 'react-color'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../../Utils'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { LinkPopup } from '../../linking/LinkPopup'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; +import { EditorView } from 'prosemirror-view'; +import './MapAnchorMenu.scss'; +import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { StrCast } from '../../../../fields/Types'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { SettingsManager } from '../../../util/SettingsManager'; + +@observer +export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: MapAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + private _disposer2: IReactionDisposer | undefined; + private _commentCont = React.createRef<HTMLButtonElement>(); + + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search + + public Center: () => void = unimplementedFunction; + // public OnClick: (e: PointerEvent) => void = unimplementedFunction; + // public OnAudio: (e: PointerEvent) => void = unimplementedFunction; + // public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; + // public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Delete: () => void = unimplementedFunction; + public LinkNote: () => void = unimplementedFunction; + // public MakeTargetToggle: () => void = unimplementedFunction; + // public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; + public get Active() { + return this._left > 0; + } + + constructor(props: Readonly<{}>) { + super(props); + + MapAnchorMenu.Instance = this; + MapAnchorMenu.Instance._canFade = false; + } + + componentWillUnmount() { + this._disposer?.(); + this._disposer2?.(); + } + + componentDidMount() { + this._disposer2 = reaction( + () => this._opacity, + opacity => { + if (!opacity) { + } + }, + { fireImmediately: true } + ); + this._disposer = reaction( + () => SelectionManager.Views().slice(), + selected => { + MapAnchorMenu.Instance.fadeOut(true); + } + ); + } + // audioDown = (e: React.PointerEvent) => { + // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + // }; + + // cropDown = (e: React.PointerEvent) => { + // setupMoveUpEvents( + // this, + // e, + // (e: PointerEvent) => { + // this.StartCropDrag(e, this._commentCont.current!); + // return true; + // }, + // returnFalse, + // e => this.OnCrop?.(e) + // ); + // }; + + static top = React.createRef<HTMLDivElement>(); + // public get Top(){ + // return this.top + // } + + render() { + const buttons = ( + <> + { + <IconButton + tooltip="Delete Pin" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + } + { + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.LinkNote} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + } + { + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + } + {/* {this.IsTargetToggler !== returnFalse && ( + <Toggle + tooltip={'Make target visibility toggle on click'} + type={Type.PRIM} + toggleType={ToggleType.BUTTON} + toggleStatus={this.IsTargetToggler()} + onClick={this.MakeTargetToggle} + icon={<FontAwesomeIcon icon="thumbtack" />} + color={SettingsManager.userColor} + /> + )} */} + </> + ); + + return this.getElement( + <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + {buttons} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index fb15520f6..242677231 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,87 +1,108 @@ -@import "../../global/globalCssVariables.scss"; +@import '../../global/globalCssVariables.scss'; .mapBox { - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - - .mapBox-infoWindow { - background-color: white; - opacity: 0.75; - padding: 12; - font-size: 17; - } + width: 100%; + height: 100%; + overflow: hidden; + display: flex; - .mapBox-overlayButton-sidebar { - background: #121721; - height: 25px; - width: 25px; - right: 5px; - display: flex; - position: absolute; - align-items: center; - justify-content: center; - border-radius: 3px; - pointer-events: all; - z-index: 1; // so it appears on top of the document's title, if shown - - box-shadow: $standard-box-shadow; - transition: 0.2s; - - &:hover{ - filter: brightness(0.85); - } + .mapBox-infoWindow { + background-color: white; + opacity: 0.75; + padding: 12; + font-size: 17; + } + .mapBox-searchbar { + display: flex; + flex-direction: row; + width: calc(100% - 40px); + .editableText-container { + width: 100%; + font-size: 16px !important; } - - .mapBox-wrapper { + input { width: 100%; - .mapBox-input { - box-sizing: border-box; - border: 1px solid transparent; - width: 240px; - height: 32px; - padding: 0 12px; - border-radius: 3px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - font-size: 14px; - outline: none; - text-overflow: ellipses; - position: absolute; - left: 50%; - margin-left: -120px; - } } + } + .mapBox-topbar { + display: flex; + flex-direction: row; + } - .mapBox-sidebar-handle { - top: 0; - //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views - width: 10px; - height: 100%; - max-height: 35px; - background: lightgray; - border-radius: 20px; - cursor:grabbing; + .mapBox-overlayButton-sidebar { + background: #121721; + height: 25px; + width: 25px; + right: 5px; + display: flex; + position: absolute; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown + + box-shadow: $standard-box-shadow; + transition: 0.2s; + + &:hover { + filter: brightness(0.85); } - .mapBox-addMarker { + } + + .mapBox-wrapper { + width: 100%; + .mapBox-input { + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; + position: absolute; left: 50%; - margin-left: 120px; - right: unset !important; - margin-top: -10; - height: max-content; - } - .searchbox { - display:none; - } - .mapBox-addMarker { - display:none; + margin-left: -120px; } + } + .mapBox-sidebar { + position: absolute; + right: 0; + height: 100%; + } + + .mapBox-sidebar-handle { + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + width: 10px; + height: 100%; + max-height: 35px; + background: lightgray; + border-radius: 20px; + cursor: grabbing; + } + .mapBox-addMarker { + left: 50%; + margin-left: 120px; + right: unset !important; + margin-top: -10; + height: max-content; + } + .searchbox { + display: none; + } + .mapBox-addMarker { + display: none; + } } .mapBox:hover { .mapBox-addMarker { - display:block; + display: block; } .searchbox { - display :block; + display: block; } } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index cf017d746..d7469e530 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,31 +1,33 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; import BingMapsReact from 'bingmaps-react'; -import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +import { Button, EditableText, IconButton, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; -import { Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; import { InkTool } from '../../../../fields/InkField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; +import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; +import { LinkManager } from '../../../util/LinkManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager } from '../../../util/UndoManager'; +import { Transform } from '../../../util/Transform'; +import { undoable, UndoManager } from '../../../util/UndoManager'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { Annotation } from '../../pdf/Annotation'; import { SidebarAnnos } from '../../SidebarAnnos'; +import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; +import { PinProps, PresBox } from '../trails'; +import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; -import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - +// amongus /** * MapBox architecture: * Main component: MapBox.tsx @@ -39,29 +41,7 @@ import { MapBoxInfoWindow } from './MapBoxInfoWindow'; * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps */ -// const _global = (window /* browser */ || global /* node */) as any; - -const mapContainerStyle = { - height: '100%', -}; - -const defaultCenter = { - lat: 42.360081, - lng: -71.058884, -}; - -const mapOptions = { - fullscreenControl: false, -}; - 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> -const apiKey = process.env.GOOGLE_MAPS; - -const script = document.createElement('script'); -script.defer = true; -script.async = true; -script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; -document.head.appendChild(script); /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -79,245 +59,56 @@ document.head.appendChild(script); // }, // }); -// options for searchbox in Google Maps Places Autocomplete API -const options = { - fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields - strictBounds: false, - types: ['establishment'], // type pf places, subject of change according to user need -} as google.maps.places.AutocompleteOptions; - @observer -export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { - static UseBing = true; - private _dropDisposer?: DragManager.DragDropDisposer; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable private _overlayAnnoInfo: Opt<Doc>; - showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); +export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); - @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.text_inlineAnnotations); - } + private _dragRef = React.createRef<HTMLDivElement>(); + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @observable private _map: google.maps.Map = null as unknown as google.maps.Map; - @observable private selectedPlace: Doc | undefined; - @observable private markerMap: { [id: string]: google.maps.Marker } = {}; - @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; @observable private _marqueeing: number[] | undefined; - @observable private _isAnnotating = false; - @observable private inputRef = React.createRef<HTMLInputElement>(); - @observable private searchMarkers: google.maps.Marker[] = []; - @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } - @computed get allMapMarkers() { + // this list contains pushpins and configs + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } - @observable private toggleAddMarker = false; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @observable _showSidebar = false; + @computed get allPushpins() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); + } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return this.layoutDoc._layout_showSidebar ? true : false; + } + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + @computed get SidebarKey() { + return this.fieldKey + '_sidebar'; } - - static _canAnnotate = true; - static _hadSelection: boolean = false; - private _sidebarRef = React.createRef<SidebarAnnos>(); - private _ref: React.RefObject<HTMLDivElement> = React.createRef(); componentDidMount() { + this._unmounting = false; this.props.setContentView?.(this); } - @action - private setSearchBox = (searchBox: any) => { - this.searchBox = searchBox; - }; - - // iterate allMarkers to size, center, and zoom map to contain all markers - private fitBounds = (map: google.maps.Map) => { - const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); - const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); - !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); - }; - - /** - * Custom control for add marker button - * @param controlDiv - * @param map - */ - private CenterControl = () => { - const controlDiv = document.createElement('div'); - controlDiv.className = 'mapBox-addMarker'; - // Set CSS for the control border. - const controlUI = document.createElement('div'); - controlUI.style.backgroundColor = '#fff'; - controlUI.style.borderRadius = '3px'; - controlUI.style.cursor = 'pointer'; - controlUI.style.marginTop = '10px'; - controlUI.style.borderRadius = '4px'; - controlUI.style.marginBottom = '22px'; - controlUI.style.textAlign = 'center'; - controlUI.style.position = 'absolute'; - controlUI.style.width = '32px'; - controlUI.style.height = '32px'; - controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; - - const plIcon = document.createElement('img'); - plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; - plIcon.style.color = 'rgb(25,25,25)'; - plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - plIcon.style.fontSize = '16px'; - plIcon.style.lineHeight = '32px'; - plIcon.style.left = '18'; - plIcon.style.top = '15'; - plIcon.style.position = 'absolute'; - plIcon.width = 14; - plIcon.height = 14; - plIcon.innerHTML = 'Add'; - controlUI.appendChild(plIcon); - - // Set CSS for the control interior. - const markerIcon = document.createElement('img'); - markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; - markerIcon.style.color = 'rgb(25,25,25)'; - markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - markerIcon.style.fontSize = '16px'; - markerIcon.style.lineHeight = '32px'; - markerIcon.style.left = '-2'; - markerIcon.style.top = '1'; - markerIcon.width = 30; - markerIcon.height = 30; - markerIcon.style.position = 'absolute'; - markerIcon.innerHTML = 'Add'; - controlUI.appendChild(markerIcon); - - // Setup the click event listeners - controlUI.addEventListener('click', () => { - if (this.toggleAddMarker === true) { - this.toggleAddMarker = false; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#fff'; - markerIcon.style.color = 'rgb(25,25,25)'; - } else { - this.toggleAddMarker = true; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#4476f7'; - markerIcon.style.color = 'rgb(255,255,255)'; - } - }); - controlDiv.appendChild(controlUI); - return controlDiv; - }; - - /** - * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list - * @param position - the LatLng position where the marker is placed - * @param map - */ - @action - private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { - const marker = new google.maps.Marker({ - position: position, - map: map, - }); - map.panTo(position); - const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); - this.addDocument(mapMarker, this.annotationKey); - }; - - _loadPending = true; - /** - * store a reference to google map instance - * setup the drawing manager on the top right corner of map - * fit map bounds to contain all markers - * @param map - */ - @action - private loadHandler = (map: google.maps.Map) => { - this._map = map; - this._loadPending = true; - const centerControlDiv = this.CenterControl(); - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); - //drawingManager.setMap(map); - // if (navigator.geolocation) { - // navigator.geolocation.getCurrentPosition( - // (position: Position) => { - // const pos = { - // lat: position.coords.latitude, - // lng: position.coords.longitude, - // }; - // this._map.setCenter(pos); - // } - // ); - // } else { - // alert("Your geolocation is not supported by browser.") - // }; - map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); - map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); - setTimeout(() => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - }, 250); - // listener to addmarker event - this._map.addListener('click', (e: MouseEvent) => { - if (this.toggleAddMarker === true) { - this.placeMarker((e as any).latLng, map); - } - }); - }; - - @action - centered = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapLat = this._map.getCenter()?.lat(); - this.dataDoc.mapLng = this._map.getCenter()?.lng(); - }; - - @action - zoomChanged = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapZoom = this._map.getZoom(); - }; - - /** - * Load and render all map markers - * @param marker - * @param place - */ - @action - private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { - place[Id] ? (this.markerMap[place[Id]] = marker) : null; - }; - - /** - * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true - * @param e - * @param place - */ - @action - private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { - // set which place was clicked - this.selectedPlace = place; - place.infoWindowOpen = true; - }; + _unmounting = false; + componentWillUnmount(): void { + this._unmounting = true; + this.deselectPin(); + this._rerenderTimeout && clearTimeout(this._rerenderTimeout); + Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); + } /** * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts @@ -326,35 +117,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * @returns */ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - console.log('print all sidebar Docs'); if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - if (doc.lat !== undefined && doc.lng !== undefined) { - const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); - if (existingMarker) { - Doc.AddDocToList(existingMarker, 'data', doc); - } else { - const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); - this.addDocument(marker, this.annotationKey); - } + let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; + if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { + existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); + } + if (existingPin) { + setTimeout(() => { + // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet + if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { + const anchor = this.getAnchor(true, undefined, existingPin); + anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' }); + doc.latitude = existingPin?.latitude; + doc.longitude = existingPin?.longitude; + } + }); } }); //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)); + return this.removeDocument(doc, annotationKey, undefined); + }; + /** * Removing documents from the sidebar * @param doc * @param sidebarKey * @returns */ - sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - return this.removeDocument(doc, sidebarKey); - }; + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey); /** * Toggle sidebar onclick the tiny comment button on the top right corner @@ -373,11 +171,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const fullWidth = this.layoutDoc[Width](); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { - this._showSidebar = true; + this.layoutDoc._layout_showSidebar = true; this.layoutDoc._width = fullWidth + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; } else { - this._showSidebar = false; + this.layoutDoc._layout_showSidebar = false; this.layoutDoc._width = mapWidth; this.layoutDoc._layout_sidebarWidthPercent = '0%'; } @@ -387,60 +185,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; - - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); - @computed get layout_sidebarWidthPercent() { - return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - - /** - * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; - * add a customized temporary marker on the map - */ - @action - private handlePlaceChanged = () => { - const place = this.searchBox.getPlace(); - - if (!place.geometry || !place.geometry.location) { - // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed - window.alert("No details available for input: '" + place.name + "'"); - return; - } - - // zoom in on the location of the search result - if (place.geometry.viewport) { - this._map.fitBounds(place.geometry.viewport); - } else { - this._map.setCenter(place.geometry.location); - this._map.setZoom(17); - } - - // customize icon => customized icon for the nature of the location selected - const icon = { - url: place.icon as string, - size: new google.maps.Size(71, 71), - origin: new google.maps.Point(0, 0), - anchor: new google.maps.Point(17, 34), - scaledSize: new google.maps.Size(25, 25), - }; - - // put temporary cutomized marker on searched location - this.searchMarkers.forEach(marker => { - marker.setMap(null); - }); - this.searchMarkers = []; - this.searchMarkers.push( - new window.google.maps.Marker({ - map: this._map, - icon, - title: place.name, - position: place.geometry.location, - }) - ); - }; + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -465,12 +210,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // TODO: Adding highlight box layer to Maps @action toggleSidebar = () => { - //1.2 * w * ? = .2 * w .2/1.2 const prevWidth = this.sidebarWidth(); this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; + createNoteAnnotation = () => { + const createFunc = undoable( + action(() => { + const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', '-linkedTo']); + if (note && this.selectedPin) { + note.latitude = this.selectedPin.latitude; + note.longitude = this.selectedPin.longitude; + note.map = this.selectedPin.map; + } + }), + 'create note annotation' + ); + if (!this.layoutDoc.layout_showSidebar) { + this.toggleSidebar(); + setTimeout(createFunc); + } else createFunc(); + }; sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; @@ -482,7 +243,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); @action onMarqueeDown = (e: React.PointerEvent) => { @@ -503,48 +264,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.rootDoc); }; - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - return this.addDocument(doc, annotationKey); - }; + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - @computed get annotationLayer() { - return ( - <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .map(anno => ( - <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> - ))} - </div> - ); - } - - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; - - /** - * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker - * @returns - */ - private renderMarkers = () => { - return this.allMapMarkers.map(place => ( - <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> - )); - }; - - // TODO: auto center on select a document in the sidebar - private handleMapCenter = (map: google.maps.Map) => { - // console.log("print the selected views in selectionManager:") - // if (SelectionManager.Views().lastElement()) { - // console.log(SelectionManager.Views().lastElement()); - // } - }; - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); @@ -581,24 +307,409 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } else { this._bingSearchManager.geocode({ where: query, - callback: action((r: any) => { - res(r.results[0].location); - }), + callback: action((r: any) => res(r.results[0].location)), errorCallback: (e: any) => reject(), }); } }); }; + @observable + bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string) + + geoDataRequestOptions = { + entityType: 'PopulatedPlace', + }; + + // incrementer: number = 0; + /* + * Creates Pushpin doc and adds it to the list of annotations + */ + @action + createPushpin = undoable((latitude: number, longitude: number, map?: string) => { + // Stores the pushpin as a MapMarkerDocument + const pushpin = Docs.Create.PushpinDocument( + NumCast(latitude), + NumCast(longitude), + false, + [], + { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } + // ,'pushpinIDamongus'+ this.incrementer++ + ); + this.addDocument(pushpin, this.annotationKey); + return pushpin; + // mapMarker.infoWindowOpen = true; + }, 'createpin'); + + // The pin that is selected + @observable selectedPin: Doc | undefined; + + @action + deselectPin = () => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + + const temp = this.selectedPin; + if (!this._unmounting) { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); + } + const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc)); + if (!this._unmounting) { + this._bingMap.current.entities.push(newpin); + } + this.map_docToPinMap.set(temp, newpin); + this.selectedPin = undefined; + this.bingSearchBarContents = this.rootDoc.map; + } + }; + + getView = async (doc: Doc) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + }; + /* + * Pushpin onclick + */ + @action + pushpinClicked = (pinDoc: Doc) => { + this.deselectPin(); + this.selectedPin = pinDoc; + this.bingSearchBarContents = pinDoc.map; + + // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); + + this.recolorPin(this.selectedPin, 'green'); + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.LinkNote = this.createNoteAnnotation; + + const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); + const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this.props.PanelHeight() / 2 + 32; + const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + /** + * Map OnClick + */ + @action + mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { + this.props.select(false); + this.deselectPin(); + }; + /* + * Updates values of layout doc to match the current map + */ + @action + mapRecentered = () => { + if ( + Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || // + Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7 + ) { + this.dataDoc.latitude = this._bingMap.current.getCenter().latitude; + this.dataDoc.longitude = this._bingMap.current.getCenter().longitude; + this.dataDoc.map = ''; + this.bingSearchBarContents = ''; + } + this.dataDoc.map_zoom = this._bingMap.current.getZoom(); + }; + /* + * Updates maptype + */ + @action + updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId()); + + /* + * For Bing Maps + * Called by search button's onClick + * Finds the geocode of the searched contents and sets location to that location + **/ + @action + bingSearch = () => { + return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => { + this.dataDoc.latitude = location.latitude; + this.dataDoc.longitude = location.longitude; + this.dataDoc.map_zoom = this._bingMap.current.getZoom(); + this.dataDoc.map = this.bingSearchBarContents; + }); + }; + + /* + * Returns doc w/ relevant info + */ + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { + /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER + const anchor = Docs.Create.ConfigDocument({ + title: 'MapAnchor:' + this.rootDoc.title, + text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location', + config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), + config_map_zoom: NumCast(this.dataDoc.map_zoom), + config_map_type: StrCast(this.dataDoc.map_type), + config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), + layout_unrendered: true, + mapPin: existingPin ?? this.selectedPin, + annotationOn: this.rootDoc, + }); + if (anchor) { + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + addAsAnnotation && this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + return anchor; + } + return this.rootDoc; + }; + + map_docToPinMap = new Map<Doc, any>(); + map_pinHighlighted = new Map<Doc, boolean>(); + /* + * Input: pin doc + * Adds MicrosoftMaps Pushpin to the map (render) + */ + @action + addPushpin = (pin: Doc) => { + const pushPin = pin.infoWindowOpen + ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {}) + : new this.MicrosoftMaps.Pushpin( + new this.MicrosoftMaps.Location(pin.latitude, pin.longitude) + // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'} + ); + + this._bingMap.current.entities.push(pushPin); + + this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); + // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); + this.map_docToPinMap.set(pin, pushPin); + }; + + /* + * Input: pin doc + * Removes pin from annotations + */ + @action + removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); + + /* + * Removes pushpin from map render + */ + deletePushpin = (pinDoc: Doc) => { + if (!this._unmounting) { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); + } + this.map_docToPinMap.delete(pinDoc); + this.selectedPin = undefined; + }; + + @action + deleteSelectedPin = undoable(() => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toString(DocCast(this.selectedPin.mapPin))}`, 'remove'); + + this.removePushpin(this.selectedPin); + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }, 'delete pin'); + + tryHideMapAnchorMenu = (e: PointerEvent) => { + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === MapAnchorMenu.top.current) return; + target = target.parentElement; + } + e.stopPropagation(); + e.preventDefault(); + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + @action + centerOnSelectedPin = () => { + if (this.selectedPin) { + this.dataDoc.latitude = this.selectedPin.latitude; + this.dataDoc.longitude = this.selectedPin.longitude; + this.dataDoc.map = this.selectedPin.map ?? ''; + this.bingSearchBarContents = this.selectedPin.map; + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); + }; + + /** + * View options for bing maps + */ bingViewOptions = { - center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng }, + // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng }, + zoom: this.dataDoc.latitude ?? 10, mapTypeId: 'grayscale', }; + + /** + * Map options + */ bingMapOptions = { navigationBarMode: 'square', + backgroundColor: '#f1f3f4', + enableInertia: true, + supportedMapTypes: ['grayscale', 'canvasLight'], + disableMapTypeSelectorMouseOver: true, + // showScalebar:true + // disableRoadView:true, + // disableBirdseye:true + streetsideOptions: { + showProblemReporting: false, + showCurrentAddress: false, + }, + }; + + @action + searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText); + + recolorPin = (pin: Doc, color?: string) => { + 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._bingMap.current.entities.push(newpin); + this.map_docToPinMap.set(pin, newpin); + }; + + /* + * Called when BingMap is first rendered + * Initializes starting values + */ + @observable _mapReady = false; + @action + bingMapReady = (map: any) => { + this._mapReady = true; + this._bingMap = map.map; + if (!this._bingMap.current) { + alert('NO Map!?'); + } + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change')); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); + + this._disposers.mapLocation = reaction( + () => this.rootDoc.map, + mapLoc => (this.bingSearchBarContents = mapLoc), + { fireImmediately: true } + ); + this._disposers.highlight = reaction( + () => 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 }) => { + if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin); + this.map_pinHighlighted.delete(pushpin); + } + }); + allConfigPins.forEach(({ doc, pushpin }) => { + if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin, 'orange'); + this.map_pinHighlighted.set(pushpin, true); + } + }); + }, + { fireImmediately: true } + ); + + this._disposers.location = reaction( + () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }), + locationObject => { + // if (this._bingMap.current) + try { + locationObject?.zoom && + this._bingMap.current?.setView({ + mapTypeId: locationObject.mapType, + zoom: locationObject.zoom, + center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng), + }); + } catch (e) { + console.log(e); + } + }, + { fireImmediately: true } + ); + }; + + dragToggle = (e: React.PointerEvent) => { + let dragClone: HTMLDivElement | undefined; + + setupMoveUpEvents( + e, + e, + e => { + if (!dragClone) { + dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; + dragClone.style.position = 'absolute'; + dragClone.style.zIndex = '10000'; + DragManager.Root().appendChild(dragClone); + } + dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; + return false; + }, + e => { + if (!dragClone) return; + DragManager.Root().removeChild(dragClone); + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === this._ref.current) { + const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); + this.createPushpin(location.latitude, location.longitude); + break; + } + target = target.parentElement; + } + }, + e => { + const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + if (this.bingSearchBarContents) { + this.bingSearch().then(createPin); + } else createPin(); + } + ); }; - bingMapReady = (map: any) => (this._bingMap = map.map); + + searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); + + static _firstRender = true; + static _rerenderDelay = 500; + _rerenderTimeout: any; render() { + // bcz: no idea what's going on here, but bings maps have some kind of bug + // such that we need to delay rendering a second map on startup until the first map is rendered. + this.rootDoc[DocCss]; + if (MapBox._rerenderDelay) { + // prettier-ignore + this._rerenderTimeout = this._rerenderTimeout ?? + setTimeout(action(() => { + if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { + MapBox._rerenderDelay = 0; + } + this._rerenderTimeout = undefined; + this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + }), MapBox._rerenderDelay); + return null; + } + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> @@ -607,47 +718,87 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onWheel={e => e.stopPropagation()} onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); - // just a simple test of bing maps geocode api - // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA'); - // this._bingMap.current.setView({ - // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, - // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), - // zoom: 15, - // }); }} - style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - - {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />} - <div style={{ display: MapBox.UseBing ? 'none' : undefined }}> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> - </Autocomplete> - - {this.renderMarkers()} - {this.allMapMarkers - .filter(marker => marker.infoWindowOpen) - .map(marker => ( - <MapBoxInfoWindow - key={marker[Id]} - {...this.props} - setContentView={emptyFunction} - place={marker} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> - ))} - {/* {this.handleMapCenter(this._map)} */} - </GoogleMap> + + <div className="mapBox-searchbar"> + <EditableText + // editing + setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} + onEnter={e => this.bingSearch()} + placeholder={this.bingSearchBarContents || 'enter city/zip/...'} + textAlign="center" + /> + <IconButton + icon={ + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF"> + <path + fill="currentColor" + d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path> + </svg> + } + onClick={this.bingSearch} + type={Type.TERT} + /> + <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}> + <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} /> + </div> </div> + + <BingMapsReact + onMapReady={this.bingMapReady} // + bingMapsKey={bingApiKey} + height="100%" + mapOptions={this.bingMapOptions} + width="100%" + viewOptions={this.bingViewOptions} + /> + <div> + {!this._mapReady + ? null + : this.allAnnotations + .filter(anno => !anno.layout_unrendered) + .map((pushpin, i) => ( + <DocumentView + key={i} + {...this.props} + renderDepth={this.props.renderDepth + 1} + Document={pushpin} + DataDoc={undefined} + PanelWidth={returnOne} + PanelHeight={returnOne} + NativeWidth={returnOne} + NativeHeight={returnOne} + onKey={undefined} + onDoubleClick={undefined} + onBrowseClick={undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + isDocumentActive={returnFalse} + isContentActive={returnFalse} + addDocTab={returnFalse} + ScreenToLocalTransform={Transform.Identity} + fitContentsToBox={undefined} + focus={returnOne} + /> + ))} + </div> + {/* <MapBoxInfoWindow + key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> */} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( <MarqueeAnnotator rootDoc={this.rootDoc} @@ -666,7 +817,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps )} </div> {/* </LoadScript > */} - <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index b3ae8242d..407a91dd0 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -98,7 +98,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public get SidebarKey() { return this.fieldKey + '_sidebar'; } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); @computed get inlineTextAnnotations() { return this.allMapMarkers.filter(a => a.text_inlineAnnotations); } @@ -229,7 +229,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps map: map, }); map.panTo(position); - const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); this.addDocument(mapMarker, this.annotationKey); }; @@ -260,7 +260,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // } else { // alert("Your geolocation is not supported by browser.") // }; - map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); + map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); setTimeout(() => { if (this._loadPending && this._map.getBounds()) { @@ -292,7 +292,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._loadPending = false; this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } - this.dataDoc.mapZoom = this._map.getZoom(); + this.dataDoc.map_zoom = this._map.getZoom(); }; /** @@ -333,7 +333,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (existingMarker) { Doc.AddDocToList(existingMarker, 'data', doc); } else { - const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); this.addDocument(marker, this.annotationKey); } } @@ -502,7 +502,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.props.Document); }; addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 577101445..66c47d131 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -47,7 +47,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); render() { return ( - <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}> + <InfoWindow + // anchor={this.props.markerMap[this.props.place[Id]]} + onCloseClick={this.handleInfoWindowClose}> <div className="mapbox-infowindow"> <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> <CollectionStackingView diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx new file mode 100644 index 000000000..42bada0ef --- /dev/null +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -0,0 +1,34 @@ +import { observer } from 'mobx-react'; +// import { SettingsManager } from '../../../util/SettingsManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; +import React = require('react'); +import { computed } from 'mobx'; +import { MapBox } from './MapBox'; + +/** + * Map Pushpin doc class + */ +@observer +export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(MapPushpinBox, fieldKey); + } + componentDidMount() { + this.mapBoxView.addPushpin(this.rootDoc); + } + componentWillUnmount() { + this.mapBoxView.deletePushpin(this.rootDoc); + } + + @computed get mapBoxView() { + return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + } + @computed get mapBox() { + return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + } + + render() { + return <div />; + } +} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 758b49655..73a5be90a 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -24,7 +24,6 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { Colors } from '../global/globalEnums'; -import { LightboxView } from '../LightboxView'; import { CreateImage } from '../nodes/WebBoxRenderer'; import { PDFViewer } from '../pdf/PDFViewer'; import { SidebarAnnos } from '../SidebarAnnos'; @@ -243,14 +242,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), annotationOn: this.rootDoc, }); - const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true); - const anchor = annoAnchor ?? docAnchor(); + const visibleAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true); + const anchor = visibleAnchor ?? docAnchor(); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML; - if (addAsAnnotation || annoAnchor) { - this.addDocument(anchor); - } + addAsAnnotation && this.addDocument(anchor); + return anchor; }; @@ -439,12 +437,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); + toggleSidebarType = () => (this.rootDoc[this.SidebarKey + '_type_collection'] = this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); + !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" }); !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); @@ -555,7 +553,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}> - {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} + {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} </div> ); } diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 8fa2861b6..481e43feb 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -1,20 +1,27 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { BoolCast, DocCast } from '../../../../fields/Types'; import { VideoField } from '../../../../fields/URLField'; import { Upload } from '../../../../server/SharedMediaTypes'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { DragManager } from '../../../util/DragManager'; +import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { Presentation } from '../../../util/TrackMovements'; +import { undoBatch } from '../../../util/UndoManager'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { ViewBoxBaseComponent } from '../../DocComponent'; +import { media_state } from '../AudioBox'; import { FieldView, FieldViewProps } from '../FieldView'; import { VideoBox } from '../VideoBox'; import { RecordingView } from './RecordingView'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { Presentation } from '../../../util/TrackMovements'; -import { Doc } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { BoolCast, DocCast } from '../../../../fields/Types'; -import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { Docs } from '../../../documents/Documents'; @observer export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -34,9 +41,7 @@ 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) => { @@ -54,6 +59,128 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); } }; + @undoBatch + @action + public static WorkspaceStopRecording() { + const remDoc = RecordingBox.screengrabber?.rootDoc; + if (remDoc) { + //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.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 + setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000); + Doc.UserDoc().workspaceRecordingState = media_state.Paused; + Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc); + } + } + + /** + * This method toggles whether or not we are currently using the RecordingBox to record with the topbar button + * @param _readOnly_ + * @returns + */ + @undoBatch + @action + public static WorkspaceStartRecording(value: string) { + const screengrabber = + value === 'Record Workspace' + ? Docs.Create.ScreenshotDocument({ + title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, + _width: 205, + _height: 115, + }) + : Docs.Create.WebCamDocument(`${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, { + title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, + _width: 205, + _height: 115, + }); + screengrabber.overlayX = 70; //was -400 + screengrabber.overlayY = 590; //was 0 + Doc.GetProto(screengrabber)[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay + DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { + RecordingBox.screengrabber = docView.ComponentView as RecordingBox; + RecordingBox.screengrabber.Record?.(); + }); + Doc.UserDoc().workspaceRecordingState = media_state.Recording; + } + + /** + * This method changes the menu depending on whether or not we are in playback mode + * @param value RecordingBox rootdoc + */ + @undoBatch + @action + public static replayWorkspace(value: Doc) { + Doc.UserDoc().currentRecording = value; + value.overlayX = 70; + value.overlayY = window.innerHeight - 180; + Doc.AddToMyOverlay(value); + DocumentManager.Instance.AddViewRenderedCb(value, docView => { + Doc.UserDoc().currentRecording = docView.rootDoc; + SelectionManager.SelectSchemaViewDoc(value); + RecordingBox.resumeWorkspaceReplaying(value); + }); + } + + /** + * Adds the recording box to the canvas + * @param value current recordingbox + */ + @undoBatch + @action + public static addRecToWorkspace(value: RecordingBox) { + let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); + (ffView?.ComponentView as CollectionFreeFormView).props.addDocument?.(value.rootDoc); + Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.rootDoc); + Doc.RemFromMyOverlay(value.rootDoc); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + } + + @action + public static resumeWorkspaceReplaying(doc: Doc) { + const docView = DocumentManager.Instance.getDocumentView(doc); + if (docView?.ComponentView instanceof VideoBox) { + docView.ComponentView.Play(); + } + Doc.UserDoc().workspaceReplayingState = media_state.Playing; + } + + @action + public static pauseWorkspaceReplaying(doc: Doc) { + const docView = DocumentManager.Instance.getDocumentView(doc); + const videoBox = docView?.ComponentView as VideoBox; + if (videoBox) { + videoBox.Pause(); + } + Doc.UserDoc().workspaceReplayingState = media_state.Paused; + } + + @action + public static stopWorkspaceReplaying(value: Doc) { + Doc.RemFromMyOverlay(value); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + Doc.RemFromMyOverlay(value); + } + + @undoBatch + @action + public static removeWorkspaceReplaying(value: Doc) { + Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value); + Doc.RemFromMyOverlay(value); + Doc.UserDoc().currentRecording = undefined; + Doc.UserDoc().workspaceReplayingState = undefined; + Doc.UserDoc().workspaceRecordingState = undefined; + } Record: undefined | (() => void); Pause: undefined | (() => void); @@ -81,28 +208,52 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } static screengrabber: RecordingBox | undefined; } -ScriptingGlobals.add(function toggleRecording(_readOnly_: boolean) { - if (_readOnly_) return RecordingBox.screengrabber ? true : false; - if (RecordingBox.screengrabber) { - RecordingBox.screengrabber.Pause?.(); - setTimeout(() => { - RecordingBox.screengrabber?.Finish?.(); - RecordingBox.screengrabber!.rootDoc.overlayX = 100; - RecordingBox.screengrabber!.rootDoc.overlayY = 100; - RecordingBox.screengrabber = undefined; - }, 100); - } else { - const screengrabber = Docs.Create.WebCamDocument('', { - _width: 384, - _height: 216, - }); - screengrabber.overlayX = -400; - screengrabber.overlayY = 0; - screengrabber[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; - Doc.AddToMyOverlay(screengrabber); - DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => { - RecordingBox.screengrabber = docView.ComponentView as RecordingBox; - RecordingBox.screengrabber.Record?.(); - }); + +ScriptingGlobals.add(function stopWorkspaceRecording() { + RecordingBox.WorkspaceStopRecording(); +}); + +ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) { + RecordingBox.stopWorkspaceReplaying(value); +}); +ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) { + RecordingBox.removeWorkspaceReplaying(value); +}); + +ScriptingGlobals.add(function getCurrentRecording() { + return Doc.UserDoc().currentRecording; +}); +ScriptingGlobals.add(function getWorkspaceRecordings() { + return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]); +}); +ScriptingGlobals.add(function isWorkspaceRecording() { + return Doc.UserDoc().workspaceRecordingState === media_state.Recording; +}); +ScriptingGlobals.add(function isWorkspaceReplaying() { + return Doc.UserDoc().workspaceReplayingState; +}); +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); +}); +ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) { + RecordingBox.pauseWorkspaceReplaying(value); +}); +ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) { + RecordingBox.resumeWorkspaceReplaying(value); +}); + +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)], 'embed'), value.e.clientX, value.e.clientY); + value.e.preventDefault(); + return true; + } +}); +ScriptingGlobals.add(function renderDropdown() { + if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) { + return true; } -}, 'toggle recording'); + return false; +}); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 0e386b093..f7ed82643 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -164,7 +164,6 @@ export function RecordingView(props: IRecordingViewProps) { const finish = () => { // call stop on the video recorder if active videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop(); - // end the streams (audio/video) to remove recording icon const stream = videoElementRef.current!.srcObject; stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 83a29f071..ebb8a3374 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -26,6 +26,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import './ScreenshotBox.scss'; import { VideoBox } from './VideoBox'; +import { TrackMovements } from '../../util/TrackMovements'; +import { media_state } from './AudioBox'; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has @@ -180,7 +182,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ref={r => { this._videoRef = r; setTimeout(() => { - if (this.rootDoc.mediaState === 'pendingRecording' && this._videoRef) { + if (this.rootDoc.mediaState === media_state.PendingRecording && this._videoRef) { this.toggleRecording(); } }, 100); @@ -219,6 +221,9 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl // } return null; } + Record = () => !this._screenCapture && this.toggleRecording(); + Pause = () => this._screenCapture && this.toggleRecording(); + toggleRecording = async () => { if (!this._screenCapture) { this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); @@ -233,9 +238,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); this._videoRec = new MediaRecorder(this._videoRef!.srcObject); const vid_chunks: any = []; - this._videoRec.onstart = () => (this.dataDoc[this.props.fieldKey + '_recordingStart'] = new DateField(new Date())); + 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) => { + const presentation = TrackMovements.Instance.yieldPresentation(); + if (presentation?.movements) { + const presCopy = { ...presentation }; + presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any; + this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy); + } + TrackMovements.Instance.finish(); const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); const [{ result }] = await Networking.UploadFilesToServer({ file }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; @@ -314,7 +329,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </> </CollectionFreeFormView> </div> - <div style={{ background: SettingsManager.Instance.userColor, position: 'relative', height: this.formattedPanelHeight() }}> + <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox {...this.props} @@ -336,8 +351,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div> </div> {!this.props.isSelected() ? null : ( - <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.Instance.userColor }}> - <div className="screenshotBox-recorder" style={{ color: SettingsManager.Instance.userBackgroundColor, background: SettingsManager.Instance.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}> + <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.userColor }}> + <div className="screenshotBox-recorder" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}> <FontAwesomeIcon icon="file" size="lg" /> </div> </div> diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 5e1359441..f803715ad 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -84,7 +84,7 @@ width: 0; height: 0; position: relative; - z-index: 100001; + z-index: 2000; } .videoBox-ui { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 2177adeff..d7f7c9b73 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -26,11 +26,10 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; -import { DocumentDecorations } from '../DocumentDecorations'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; +import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; @@ -58,6 +57,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp static _youtubeIframeCounter: number = 0; static heightPercent = 80; // height of video relative to videoBox when timeline is open static numThumbnails = 20; + private unmounting = false; private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; // <video> ref @@ -125,6 +125,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } componentDidMount() { + this.unmounting = false; this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. if (this.youtubeVideoId) { const youtubeaspect = 400 / 315; @@ -136,7 +137,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } - this.player && this.setPlayheadTime(0); + this.player && this.setPlayheadTime(this.timeline.clipStart || 0); document.addEventListener('keydown', this.keyEvents, true); if (this.presentation) { @@ -145,6 +146,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } componentWillUnmount() { + this.unmounting = true; this.removeCurrentlyPlaying(); this.Pause(); Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); @@ -192,7 +194,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._finished = false; start = this.timeline.trimStart; } - try { this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime); update && this.player && this.playFrom(start, undefined, true); @@ -387,7 +388,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; - const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc; + const anchor = addAsAnnotation + ? CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc + : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc); return anchor; }; @@ -409,7 +412,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // updates video time @action updateTimecode = () => { - this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); + !this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); try { this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.()); } catch (e) { @@ -624,7 +627,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), { fireImmediately: true } ); @@ -848,11 +851,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp zoom = (zoom: number) => this.timeline?.setZoom(zoom); // plays link - playLink = (doc: Doc) => { - const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0); + playLink = (doc: Doc, options: DocFocusOptions) => { + const startTime = Math.max(0, NumCast(doc.config_clipStart, this._stackedTimeline?.anchorStart(doc) || 0)); const endTime = this.timeline?.anchorEnd(doc); if (startTime !== undefined) { - if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (options.playMedia) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } }; @@ -1040,7 +1043,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.props.bringToFront(cropping); return cropping; }; - savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -1078,8 +1080,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp fieldKey={this.annotationKey} isAnnotationOverlay={true} annotationLayerHostsContent={true} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} childFilters={this.timelineDocFilter} @@ -1120,7 +1122,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get UIButtons() { const bounds = this.props.docViewPath().lastElement().getBounds(); const width = (bounds?.right || 0) - (bounds?.left || 0); - const curTime = NumCast(this.layoutDoc._layout_currentTimecode) - (this.timeline?.clipStart || 0); + const curTime = NumCast(this.layoutDoc._layout_currentTimecode); return ( <> <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> @@ -1129,7 +1131,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.timeline && width > 150 && ( <div className="timecode-controls"> - <div className="timecode-current">{formatTime(curTime)}</div> + <div className="timecode-current">{formatTime(curTime - (this.timeline?.clipStart || 0))}</div> {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( <div className="timeline-slider"> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index febf8341e..27c19105f 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -50,7 +50,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; static webStyleSheet = addStyleSheet(); - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -196,8 +196,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); this._disposers.urlchange = reaction( () => WebCast(this.rootDoc.data), + url => this.submitURL(false, false) + ); + this._disposers.titling = reaction( + () => StrCast(this.rootDoc.title), url => { - this.submitURL(url.url.href, false, false); + url.startsWith('www') && this.setData('http://' + url); + url.startsWith('http') && this.setData(url); } ); @@ -325,8 +330,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ele.append(contents); } } catch (e) {} + const visibleAnchor = this._getAnchor(this._savedAnnotations, false); const anchor = - this._getAnchor(this._savedAnnotations, false) ?? + visibleAnchor ?? Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), @@ -335,8 +341,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML; - //addAsAnnotation && - this.addDocumentWrapper(anchor); + addAsAnnotation && this.addDocumentWrapper(anchor); return anchor; }; @@ -364,14 +369,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action iframeDown = (e: PointerEvent) => { - const sel = this._iframe?.contentWindow?.getSelection?.(); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale; const word = getWordAtPoint(e.target, e.clientX, e.clientY); - this._setPreviewCursor?.(e.clientX, e.clientY, false, true); + this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]); - if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { + if (word || (e.target as any)?.className?.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { setTimeout( action(() => (this._marqueeing = undefined)), 100 @@ -417,6 +421,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } _iframetimeout: any = undefined; + @observable _warning = 0; @action iframeLoaded = (e: any) => { const iframe = this._iframe; @@ -430,6 +435,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps try { href = iframe?.contentWindow?.location.href; } catch (e) { + runInAction(() => this._warning++); href = undefined; } let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); @@ -462,12 +468,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // { passive: false } // ); const initHeights = () => { - this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); + this._scrollHeight = Math.max(this._scrollHeight, iframeContent.body.scrollHeight || 0); if (this._scrollHeight) { this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight); this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); } }; + const swidth = Math.max(NumCast(this.layoutDoc.nativeWidth), iframeContent.body.scrollWidth || 0); + if (swidth) { + const aspectResize = swidth / NumCast(this.rootDoc.nativeWidth); + this.rootDoc.nativeWidth = swidth; + this.rootDoc.nativeHeight = NumCast(this.rootDoc.nativeHeight) * aspectResize; + this.layoutDoc.height = this.layoutDoc[Height]() * aspectResize; + } initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); this._iframetimeout = setTimeout( @@ -545,7 +558,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { if (this._outerRef.current) { - const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc); this.setDashScrollTop(scrollTop, duration); @@ -610,9 +622,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; @action - submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { - if (!newUrl) return; - if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; + submitURL = (preview?: boolean, dontUpdateIframe?: boolean) => { try { if (!preview) { if (this._webPageHasBeenRendered) { @@ -668,9 +678,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps !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.allowScripts ? 'Prevent' : 'Allow') + ' Scripts', + description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts', event: () => { - this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; + this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts']; if (this._iframe) { runInAction(() => (this._hackHide = true)); setTimeout(action(() => (this._hackHide = false))); @@ -719,7 +729,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (sel?.empty) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox if (x !== undefined && y !== undefined) { - this._setPreviewCursor?.(x, y, false, false); + this._setPreviewCursor?.(x, y, false, false, this.rootDoc); ContextMenu.Instance.closeMenu(); ContextMenu.Instance.setIgnoreEvents(false); if (e?.button === 2 || e?.altKey) { @@ -744,16 +754,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } if (field instanceof WebField) { const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.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); return ( <iframe + key={this._warning} className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} + style={{ pointerEvents: this._isAnyChildContentActive || DocumentView.Interacting ? 'none' : undefined }} src={url} onLoad={this.iframeLoaded} scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document. // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; - sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} + sandbox={`${scripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} /> ); } @@ -993,7 +1007,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + 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.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 48f4c2afd..c5167461b 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -8,7 +8,6 @@ import { NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { ColorScheme } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { DocFocusOptions, DocumentView } from '../DocumentView'; import { FormattedTextBox } from './FormattedTextBox'; @@ -22,7 +21,7 @@ export class DashDocView { this.dom = document.createElement('span'); this.dom.style.position = 'relative'; this.dom.style.textIndent = '0'; - this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'lightGray'); + this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, 'lightGray'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block'; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 200d06a0b..6a92d09d9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,9 +11,10 @@ import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -25,11 +26,10 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; -import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { Networking } from '../../../Network'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -38,7 +38,7 @@ import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; @@ -49,6 +49,7 @@ import { DocumentButtonBar } from '../../DocumentButtonBar'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; @@ -70,9 +71,10 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; -import { BsMarkdownFill } from 'react-icons/bs'; -const translateGoogleApi = require('translate-google-api'); +import { media_state } from '../AudioBox'; +import { setCORS } from 'google-translate-api-browser'; +// setting up cors-anywhere server address +const translate = setCORS('http://cors-anywhere.herokuapp.com/'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @@ -94,6 +96,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps static _userStyleSheet: any = addStyleSheet(); static _hadSelection: boolean = false; private _sidebarRef = React.createRef<SidebarAnnos>(); + private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; @@ -154,11 +157,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } - @computed get _recording() { - return this.dataDoc?.mediaState === 'recording'; + @computed get _recordingDictation() { + return this.dataDoc?.mediaState === media_state.Recording; } - set _recording(value) { - !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? 'recording' : undefined); + set _recordingDictation(value) { + !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { this._keymap = buildKeymap(schema, this.props); @@ -266,19 +269,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); + setTimeout(() => { const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; - DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); + let stopFunc: any; + Doc.GetProto(target).mediaState = media_state.Recording; + Doc.GetProto(target).audioAnnoState = 'recording'; + DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + let reactionDisposer = reaction( + () => target.mediaState, + action(dictation => { + if (!dictation) { + Doc.GetProto(target).audioAnnoState = 'stopped'; + stopFunc(); + reactionDisposer(); + } + }) + ); target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); } }); }; - AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); - return undefined; - }); + AnchorMenu.Instance.Highlight = undoable( + action((color: string, isLinkButton: boolean) => { + this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + return undefined; + }), + 'highlght text' + ); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -373,7 +393,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let link; LinkManager.Links(this.dataDoc).forEach((l, i) => { const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') { + if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; link = l; @@ -440,7 +460,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { - this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : ''); + this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); if (str.startsWith('@') && str.length > 1) { Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc); } @@ -575,7 +595,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps draggedDoc._freeform_fitContentsToBox = true; Doc.SetContainer(draggedDoc, this.rootDoc); const view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + try { + const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; + pos && view.dispatch(view.state.tr.insert(pos, node)); + added = pos ? true : false; // pos will be null if you don't drop onto an actual text location + } catch (e) { + console.log('Drop failed', e); + added = false; + } } } } // otherwise, fall through to outer collection to handle drop @@ -642,7 +669,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); } if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); @@ -682,7 +709,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps toggleSidebar = (preview: boolean = false) => { const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; - else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + else { + this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + } this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; @@ -838,7 +868,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); - uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); + !Doc.noviceMode && uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ description: 'Broadcast Message', @@ -939,14 +969,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; breakupDictation = () => { - if (this._editorView && this._recording) { + if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; const state = this._editorView.state; const to = state.selection.to; const updated = TextSelection.create(state.doc, to, to); this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to)); - if (this._recording) { + if (this._recordingDictation) { this.recordDictation(); } } @@ -1119,7 +1149,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps componentDidMount() { !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); - this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); + this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => this.layout_autoHeight, layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight() @@ -1236,13 +1266,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!this.props.dontRegisterView) { this._disposers.record = reaction( - () => this._recording, + () => this._recordingDictation, () => { this.stopDictation(true); - this._recording && this.recordDictation(); - } + this._recordingDictation && this.recordDictation(); + }, + { fireImmediately: true } ); - if (this._recording) setTimeout(this.recordDictation); + if (this._recordingDictation) setTimeout(this.recordDictation); } var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( @@ -1540,8 +1571,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action componentWillUnmount() { - if (this._recording) { - this._recording = !this._recording; + if (this._recordingDictation) { + this._recordingDictation = !this._recordingDictation; } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); @@ -1555,7 +1586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown = (e: React.PointerEvent): void => { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return e.stopPropagation(); + return; //e.stopPropagation(); } else (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); @@ -1579,7 +1610,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }); } - if (this._recording && !e.ctrlKey && e.button === 0) { + if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } this._downX = e.clientX; @@ -1587,10 +1618,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBoxComment.textBox = this; if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { - // stop propagation if not in sidebar - // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView, - // but that's changed, so this shouldn't be needed. - //e.stopPropagation(); // if the text box is selected, then it consumes all down events + // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. + e.stopPropagation(); // if the text box's content is active, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); } } @@ -1608,8 +1637,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - // !this.props.isSelected(true) && - editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + let xpos = pcords?.pos || 0; + while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { + xpos = xpos - 1; + } + editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(xpos)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); @@ -1752,13 +1784,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView!.state; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - if (this.layoutDoc.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { + if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { try { - translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => { + translate(curText, { from: 'en', to: 'es' }).then((result1: any) => { setTimeout( () => - translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => { - this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0]; + translate(result1.text, { from: 'es', to: 'en' }).then((result: any) => { + const tb = this._sidebarTagRef.current as FormattedTextBox; + tb._editorView?.dispatch(tb._editorView!.state.tr.insertText(result1.text + '\r\n\r\n' + result.text)); }), 1000 ); @@ -1796,7 +1829,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - e.stopPropagation(); + let stopPropagation = true; for (var i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) { @@ -1815,6 +1848,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps case 'Tab': e.preventDefault(); break; + case 'c': + this._editorView?.state.selection.empty && (stopPropagation = false); + break; default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': @@ -1824,6 +1860,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } break; } + if (stopPropagation) e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1881,7 +1918,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); @computed get audioHandle() { - return !this._recording ? null : ( + return !this._recordingDictation ? null : ( <div className="formattedTextBox-dictation" onPointerDown={e => @@ -1890,7 +1927,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e, returnFalse, emptyFunction, - action(e => (this._recording = !this._recording)) + action(e => (this._recordingDictation = !this._recordingDictation)) ) }> <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> @@ -1942,6 +1979,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> <ComponentTag {...this.props} + ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -1964,14 +2002,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps fitContentsToBox={this.fitContentsToBox} noSidebar={true} treeViewHideTitle={true} - fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} + fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> ); }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} + {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} </div> ); } @@ -2019,9 +2057,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { + if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { + if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { + // if the scrolled freeform is a child of the sidebar component, we need to let the event go through + // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling + e.preventDefault(); + } + return; + } + // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this.props.isContentActive() && !this.props.allowScroll) { - if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault(); + // prevent default if selected || child is active but this doc isn't scrollable + if ( + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(Number(this.layoutDoc._height)) && // + (this.props.isSelected() || this.isAnyChildContentActive()) + ) { + e.preventDefault(); + } e.stopPropagation(); } }; @@ -2092,10 +2145,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer`} + className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2113,9 +2166,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps /> </div> {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? null : this.sidebarHandle} + {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} - {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null} + {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 9c46459b0..e3ac4fb9d 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -21,6 +21,7 @@ import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; import { EquationBox } from '../EquationBox'; +import { numberRange } from '../../../../Utils'; const { toggleMark } = require('prosemirror-commands'); @observer @@ -149,19 +150,31 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { if (mark) { - const node = (state.selection as NodeSelection).node; + const liFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.list_item); + const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); + const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); + const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); + const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from; + const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; - const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); - dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); - } else if (dontToggle) { - const tr = state.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 - } else { - toggleMark(mark.type, mark.attrs)(state, dispatch); + const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); + } + { + const state = this.view?.state; + const tr = this.view?.state.tr; + if (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 + } else { + toggleMark(mark.type, mark.attrs)(state, dispatch); + } + } } } }; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 8bafc2cef..3e2afd2ce 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -280,11 +280,8 @@ export class RichTextRules { this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } const target = DocServer.FindDocByTitle(docTitle); - if (target) { - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target[Id], hideKey: false }); - return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); - } - return state.tr; + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); + return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 7e17008bb..6f07588b3 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -348,7 +348,7 @@ export const marks: { [index: string]: MarkSpec } = { excludes: 'user_mark', group: 'inline', toDOM(node: any) { - const uid = node.attrs.userid.replace('.', '').replace('@', ''); + const uid = node.attrs.userid.replace(/\./g, '').replace(/@/g, ''); const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 1400d0f6d..1ec6d6e3f 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -1,26 +1,25 @@ -import './GenerativeFill.scss'; -import React = require('react'); -import { useEffect, useRef, useState } from 'react'; -import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; -import { BrushHandler } from './generativeFillUtils/BrushHandler'; -import { IconButton } from 'browndash-components'; import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; -import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; -import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; -import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import { IoMdUndo, IoMdRedo } from 'react-icons/io'; -import { MainView } from '../../MainView'; +import { IconButton } from 'browndash-components'; +import { useEffect, useRef, useState } from 'react'; +import { CgClose } from 'react-icons/cg'; +import { IoMdRedo, IoMdUndo } from 'react-icons/io'; import { Doc, DocListCast } from '../../../../fields/Doc'; -import { Networking } from '../../../Network'; -import { Utils } from '../../../../Utils'; -import { DocUtils, Docs } from '../../../documents/Documents'; +import { List } from '../../../../fields/List'; import { NumCast } from '../../../../fields/Types'; +import { Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { Networking } from '../../../Network'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { OpenWhereMod } from '../DocumentView'; -import Buttons from './GenerativeFillButtons'; -import { List } from '../../../../fields/List'; -import { CgClose } from 'react-icons/cg'; import { ImageBox } from '../ImageBox'; +import './GenerativeFill.scss'; +import Buttons from './GenerativeFillButtons'; +import { BrushHandler } from './generativeFillUtils/BrushHandler'; +import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; +import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; +import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; +import { PointerHandler } from './generativeFillUtils/PointerHandler'; +import React = require('react'); enum BrushStyle { ADD, diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index bf56b4d9e..0b51813a5 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -121,7 +121,7 @@ .dropdown.active { transform: rotate(180deg); color: $light-blue; - opacity: 0.8; + opacity: 0.7; } .presBox-radioButtons { @@ -187,9 +187,6 @@ font-size: 11; font-weight: 200; height: 20; - background-color: $white; - color: $black; - border: solid 1px $black; display: flex; margin-left: 5px; margin-top: 5px; @@ -210,13 +207,11 @@ .ribbon-propertyUpDownItem { cursor: pointer; - color: white; display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; - background: $black; } .ribbon-propertyUpDownItem:hover { @@ -609,7 +604,6 @@ font-weight: 200; height: 20; background-color: $white; - border: solid 1px rgba(0, 0, 0, 0.5); display: flex; color: $black; margin-top: 5px; @@ -691,16 +685,19 @@ padding-right: 5px; padding-top: 3; padding-bottom: 3; + opacity: 0.8; } .presBox-dropdownOption:hover { position: relative; - background-color: lightgrey; + opacity: 1; + font-weight: bold; } .presBox-dropdownOption.active { position: relative; - background-color: $light-blue; + opacity: 1; + font-weight: bold; } .presBox-dropdownOptions { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index f61563f4c..2a3b232bd 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; @@ -13,9 +13,9 @@ 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, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; +import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -28,6 +28,7 @@ import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; +import { TreeView } from '../../collections/TreeView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; @@ -52,6 +53,7 @@ export interface pinDataTypes { dataview?: boolean; poslayoutview?: boolean; dataannos?: boolean; + map?: boolean; } export interface PinProps { audioRange?: boolean; @@ -279,6 +281,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return listItems.filter(doc => !doc.layout_unrendered); } }; + + // go to documents chain + runSubroutines = (childrenToRun: Doc[], normallyNextSlide: Doc) => { + console.log(childrenToRun, normallyNextSlide, 'runSUBFUNC'); + if (childrenToRun === undefined) { + console.log('children undefined'); + return; + } + if (childrenToRun[0] === normallyNextSlide) { + return; + } + + childrenToRun.forEach(child => { + DocumentManager.Instance.showDocument(child, {}); + }); + }; + // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { @@ -319,6 +338,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]); const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; + + // before moving onto next slide, run the subroutines :) + const currentDoc = this.childDocs[this.itemIndex]; + //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); progressiveReveal(true); // shows first progressive document, but without a transition effect } else { @@ -364,11 +389,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } - if (from?.mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) { + if (from?.presentation_mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) { this.stopTempMedia(from.presentation_targetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played - if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') { + if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.presentation_mediaStart === 'auto') { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); @@ -383,6 +408,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform); + const map = [DocumentType.MAP].includes(targetType); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); const clippable = [DocumentType.COMPARISON].includes(targetType); const datarange = [DocumentType.FUNCPLOT].includes(targetType); @@ -392,7 +418,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action @@ -460,6 +486,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + if (pinDataTypes?.map || (!pinDataTypes && activeItem.config_latitude !== undefined)) { + if (bestTarget.latitude !== activeItem.config_latitude) { + Doc.SetInPlace(bestTarget, 'latitude', NumCast(activeItem.config_latitude), true); + changed = true; + } + if (bestTarget.longitude !== activeItem.config_longitude) { + Doc.SetInPlace(bestTarget, 'longitude', NumCast(activeItem.config_longitude), true); + changed = true; + } + if (bestTarget.zoom !== activeItem.config_map_zoom) { + Doc.SetInPlace(bestTarget, 'map_zoom', NumCast(activeItem.config_map_zoom), true); + changed = true; + } + if (bestTarget.map_type !== activeItem.config_map_type) { + Doc.SetInPlace(bestTarget, 'map_type', StrCast(activeItem.config_map_type), true); + changed = true; + } + if (bestTarget.map !== activeItem.config_map) { + Doc.SetInPlace(bestTarget, 'map', StrCast(activeItem.config_map), true); + changed = true; + } + } if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.config_clipStart !== undefined)) { if (bestTarget._layout_currentTimecode !== activeItem.config_clipStart) { bestTarget._layout_currentTimecode = activeItem.config_clipStart; @@ -571,7 +619,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; - const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); + const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime); } @@ -640,6 +688,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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>( DocListCast(targetDoc[fkey] as ObjectField).map(d => @@ -680,6 +735,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); } } + + @action + static reversePin(pinDoc: Doc, targetDoc: Doc) { + // const fkey = Doc.LayoutFieldKey(targetDoc); + pinDoc.config_data = targetDoc.data; + + console.log(pinDoc.presData); + } + /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -723,7 +787,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const options: DocFocusOptions = { willPan: activeItem.presentation_movement !== PresMovement.None, willZoomCentered: activeItem.presentation_movement === PresMovement.Zoom || activeItem.presentation_movement === PresMovement.Jump || activeItem.presentation_movement === PresMovement.Center, - zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1), + zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.config_zoom, 1), zoomTime: activeItem.presentation_movement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, @@ -732,8 +796,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, zoomTextSelections: BoolCast(activeItem.presentation_zoomText), playAudio: BoolCast(activeItem.presPlayAudio), + playMedia: activeItem.presentation_mediaStart === 'auto', }; - if (activeItem.presOpenInLightbox) { + if (activeItem.presentation_openInLightbox) { const context = DocCast(targetDoc.annotationOn) ?? targetDoc; if (!DocumentManager.Instance.getLightboxDocumentView(context)) { LightboxView.SetLightboxDoc(context); @@ -1009,8 +1074,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); if (audio) { - audio.mediaStart = 'manual'; - audio.mediaStop = 'manual'; audio.config_clipStart = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */)); audio.config_clipEnd = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */)); audio.presentation_duration = audio.config_clipStart - audio.config_clipEnd; @@ -1086,7 +1149,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; } - } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); + } else { + this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); + } this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; @@ -1309,7 +1374,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get paths() { let pathPoints = ''; this.childDocs.forEach((doc, index) => { - const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); + 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; @@ -1362,7 +1427,7 @@ 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.presZoom = scale)); + this.selectedArray.forEach(doc => (doc.config_zoom = scale)); }; /* @@ -1404,8 +1469,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action updateOpenDoc = (activeItem: Doc) => { - activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox; - this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox)); + activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox; + this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox)); }; @undoBatch @@ -1424,6 +1489,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { 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 @@ -1433,13 +1502,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { max={max} value={value} readOnly={true} - style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); + document.addEventListener('pointerup', PresBox.endBatch, true); e.stopPropagation(); }} - onPointerUp={() => PresBox._sliderBatch.end()} onChange={e => { e.stopPropagation(); change(e.target.value); @@ -1464,7 +1533,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); }; - @computed get visibiltyDurationDropdown() { + @computed get visibilityDurationDropdown() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { const targetType = this.targetDoc.type; @@ -1473,30 +1542,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="presBox-ribbon"> <div className="ribbon-doubleButton"> - <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}> + <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHideBefore(activeItem)}> Hide before </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}> + <div + className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHide(activeItem)}> Hide </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHideAfter(activeItem)}> Hide after </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}> + <div + className="ribbon-toggle" + style={{ + border: `solid 1px ${SettingsManager.userColor}`, + color: SettingsManager.userColor, + background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + }} + onClick={() => this.updateOpenDoc(activeItem)}> Lightbox </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}> - <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}> + <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}> + <div + className="ribbon-toggle" + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateEaseFunc(activeItem)}> {`${StrCast(activeItem.presEaseFunc, 'ease')}`} </div> </Tooltip> @@ -1505,10 +1593,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <> <div className="ribbon-doubleButton"> <div className="presBox-subheading">Slide Duration</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1547,7 +1635,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Progressivize Collection</div> <input className="presBox-checkbox" - style={{ margin: 10 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => { activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; @@ -1570,7 +1658,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Progressivize First Bullet</div> <input className="presBox-checkbox" - style={{ margin: 10 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)} checked={!NumCast(activeItem.presentation_indexedStart)} @@ -1578,7 +1666,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Expand Current Bullet</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} + checked={BoolCast(activeItem.presBulletExpand)} + /> </div> <div className="ribbon-box"> @@ -1589,10 +1683,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openBulletEffectDropdown = !this._openBulletEffectDropdown; })} - style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, + border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {effect?.toString()} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> + <div + className={'presBox-dropdownOptions'} + style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} + onPointerDown={e => e.stopPropagation()}> {bulletEffect(PresEffect.None)} {bulletEffect(PresEffect.Fade)} {bulletEffect(PresEffect.Flip)} @@ -1623,7 +1725,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? Colors.LIGHT_BLUE : 'black'; + const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SettingsManager.userVariantColor : SettingsManager.userColor; return ( <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div @@ -1636,7 +1738,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; - const zoom = NumCast(activeItem.presZoom, 1) * 100; + const zoom = NumCast(activeItem.config_zoom, 1) * 100; const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None; return ( <div @@ -1657,10 +1759,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openMovementDropdown = !this._openMovementDropdown; })} - style={{ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, border: this._openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, + border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {this.movementName(activeItem)} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}> + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + onPointerDown={StopEvent} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + display: this._openMovementDropdown ? 'grid' : 'none', + }}> {presMovement(PresMovement.None)} {presMovement(PresMovement.Center)} {presMovement(PresMovement.Zoom)} @@ -1670,10 +1785,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1685,10 +1800,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)} <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> - <div className="ribbon-property"> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> - <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> <FontAwesomeIcon icon={'caret-up'} /> </div> @@ -1708,13 +1823,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Effects <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Play Audio Annotation</div> - <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} checked={BoolCast(activeItem.presPlayAudio)} /> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + checked={BoolCast(activeItem.presPlayAudio)} + /> </div> <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Zoom Text Selections</div> <input className="presBox-checkbox" - style={{ margin: 10 }} + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} type="checkbox" onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} checked={BoolCast(activeItem.presentation_zoomText)} @@ -1726,10 +1847,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openEffectDropdown = !this._openEffectDropdown; })} - style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, + border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> {effect?.toString()} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + display: this._openEffectDropdown ? 'grid' : 'none', + }} + onPointerDown={e => e.stopPropagation()}> {preseEffect(PresEffect.None)} {preseEffect(PresEffect.Fade)} {preseEffect(PresEffect.Flip)} @@ -1740,7 +1874,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property">{StrCast(this.activeItem.presentation_effectDirection)}</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> + {StrCast(this.activeItem.presentation_effectDirection)} + </div> </div> <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} @@ -1762,8 +1898,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get mediaOptionsDropdown() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { - const clipStart = NumCast(activeItem.clipStart); - const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration'])); + 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); return ( <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div> @@ -1774,7 +1912,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slider-text" style={{ fontWeight: 500 }}> Start time (s) </div> - <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <div id="startTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> <input className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} @@ -1782,9 +1920,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { readOnly={true} value={NumCast(activeItem.config_clipStart).toFixed(2)} onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.config_clipStart = Number(e.target.value); - })} + onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))} /> </div> </div> @@ -1792,25 +1928,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="slider-text" style={{ fontWeight: 500 }}> Duration (s) </div> - <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> - {Math.round((NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart)) * 10) / 10} + <div className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> + {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10} </div> </div> <div className="slider-block"> <div className="slider-text" style={{ fontWeight: 500 }}> End time (s) </div> - <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <div id="endTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> <input className="presBox-input" onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" readOnly={true} - value={NumCast(activeItem.config_clipEnd).toFixed(2)} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.config_clipEnd = Number(e.target.value); - })} + value={config_clipEnd.toFixed(2)} + onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))} /> </div> </div> @@ -1821,16 +1955,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { step="0.1" min={clipStart} max={clipEnd} - value={NumCast(activeItem.config_clipEnd)} - style={{ gridColumn: 1, gridRow: 1 }} + value={config_clipEnd} + style={{ gridColumn: 1, gridRow: 1, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} className={`toolbar-slider ${'end'}`} id="toolbar-slider" onPointerDown={e => { this._batch = UndoManager.StartBatch('config_clipEnd'); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.color = Colors.LIGHT_GRAY; - endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + endBlock.style.backgroundColor = SettingsManager.userVariantColor; } e.stopPropagation(); }} @@ -1838,8 +1971,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const endBlock = document.getElementById('endTime'); if (endBlock) { - endBlock.style.color = Colors.BLACK; - endBlock.style.backgroundColor = Colors.LIGHT_GRAY; + endBlock.style.backgroundColor = SettingsManager.userBackgroundColor; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1860,8 +1992,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch = UndoManager.StartBatch('config_clipStart'); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.color = Colors.LIGHT_GRAY; - startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + startBlock.style.backgroundColor = SettingsManager.userVariantColor; } e.stopPropagation(); }} @@ -1869,8 +2000,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._batch?.end(); const startBlock = document.getElementById('startTime'); if (startBlock) { - startBlock.style.color = Colors.BLACK; - startBlock.style.backgroundColor = Colors.LIGHT_GRAY; + startBlock.style.backgroundColor = SettingsManager.userBackgroundColor; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1890,22 +2020,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presBox-subheading">Start playing:</div> <div className="presBox-radioButtons"> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} /> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStart = 'manual')} + checked={activeItem.presentation_mediaStart === 'manual'} + /> <div>On click</div> </div> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} /> + <input + className="presBox-checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presentation_mediaStart = 'auto')} + checked={activeItem.presentation_mediaStart === 'auto'} + /> <div>Automatically</div> </div> </div> <div className="presBox-subheading">Stop playing:</div> <div className="presBox-radioButtons"> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} /> - <div>At audio end time</div> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStop = 'manual')} + checked={activeItem.presentation_mediaStop === 'manual'} + /> + <div>At media end time</div> </div> <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} /> + <input + className="presBox-checkbox" + type="checkbox" + style={{ border: `solid 1px ${SettingsManager.userColor}` }} + onChange={() => (activeItem.presentation_mediaStop = 'auto')} + checked={activeItem.presentation_mediaStop === 'auto'} + /> <div>On slide change</div> </div> {/* <div className="checkbox-container"> @@ -2153,8 +2307,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const activeColor = Colors.LIGHT_BLUE; - const inactiveColor = Colors.WHITE; + const activeColor = SettingsManager.userVariantColor; + const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor; return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : ( <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)}> @@ -2164,7 +2318,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <Tooltip title={<div className="dash-tooltip">View paths</div>}> <div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} - className={'toolbar-button'} + className="toolbar-button" onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}> <FontAwesomeIcon icon={'exchange-alt'} /> </div> diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 4f95f0c1f..9ac2b5a94 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -4,6 +4,10 @@ $light-background: #ececec; $slide-background: #d5dce2; $slide-active: #5b9fdd; +.testingv2 { + background-color: red; +} + .presItem-container { cursor: grab; display: flex; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 529a5024f..82ed9e8d5 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; @@ -25,6 +25,9 @@ import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; import React = require('react'); +import { TreeView } from '../../collections/TreeView'; +import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; +import { MultiToggle, Type } from 'browndash-components'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -301,8 +304,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.config_rotation = NumCast(targetDoc.rotation); activeItem.config_width = NumCast(targetDoc.width); activeItem.config_height = NumCast(targetDoc.height); - activeItem.config_pinLayout = true; + activeItem.config_pinLayout = !activeItem.config_pinLayout; + // activeItem.config_pinLayout = true; }; + + //wait i dont think i have to do anything here since by default it'll revert to the previously saved if I don't save + //so basically, don't have an onClick for this, just let it do nada for now + @undoBatch + @action + revertToPreviouslySaved = (presTargetDoc: Doc, activeItem: Doc) => { + const target = DocCast(activeItem.annotationOn) ?? activeItem; + PresBox.reversePin(activeItem, target); + }; + /** * Method called for updating the view of the currently selected document * @@ -399,6 +413,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; + @undoBatch + @action + lfg = (e: React.MouseEvent) => { + e.stopPropagation(); + // TODO: fix this bug + const { toggleChildrenRun } = this.rootDoc; + TreeView.ToggleChildrenRun.get(this.rootDoc)?.(); + + // call this.rootDoc.recurChildren() to get all the children + // if (iconClick) PresElementBox.showVideo = false; + }; + @computed get toolbarWidth(): number { const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); @@ -409,13 +435,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get presButtons() { - const presBox = this.presBox; //presBox - const presBoxColor: string = StrCast(presBox?._backgroundColor); - const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc: Doc = this.targetDoc; - const activeItem: Doc = this.rootDoc; + const presBox = this.presBox; + const presBoxColor = StrCast(presBox?._backgroundColor); + const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; + const targetDoc = this.targetDoc; + const activeItem = this.rootDoc; + const hasChildren = BoolCast(this.rootDoc?.hasChildren); const items: JSX.Element[] = []; + items.push( <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}> <div @@ -486,6 +514,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> ); + if (!Doc.noviceMode && hasChildren) { + // TODO: replace with if treeveiw, has childrenDocs + items.push( + <Tooltip key="children" title={<div className="dash-tooltip">Run child processes (tree only)</div>}> + <div + className="slideButton" + onClick={e => { + e.stopPropagation(); + this.lfg(e); + }} + style={{ fontWeight: 700 }}> + <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}> @@ -532,7 +576,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ) : ( <div ref={this._dragRef} - className={`presItem-slide ${isCurrent ? 'active' : ''}`} + className={`presItem-slide ${isCurrent ? 'active' : ''}${this.rootDoc.runProcess ? ' testingv2' : ''}`} style={{ display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index b0924888a..85c1cce8c 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,85 +1,37 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState } from 'react-color'; import { Doc, Opt } from '../../../fields/Doc'; +import { StrCast } from '../../../fields/Types'; import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils'; +import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; +import { DocumentType } from '../../documents/DocumentTypes'; import { SelectionManager } from '../../util/SelectionManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu'; -import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; -import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; -import { LightboxView } from '../LightboxView'; -import { EditorView } from 'prosemirror-view'; import './AnchorMenu.scss'; -import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { StrCast } from '../../../fields/Types'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { SettingsManager } from '../../util/SettingsManager'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: AnchorMenu; private _disposer: IReactionDisposer | undefined; - private _disposer2: IReactionDisposer | undefined; - private _commentCont = React.createRef<HTMLButtonElement>(); - private _palette = [ - 'rgba(208, 2, 27, 0.8)', - 'rgba(238, 0, 0, 0.8)', - 'rgba(245, 166, 35, 0.8)', - 'rgba(248, 231, 28, 0.8)', - 'rgba(245, 230, 95, 0.616)', - 'rgba(139, 87, 42, 0.8)', - 'rgba(126, 211, 33, 0.8)', - 'rgba(65, 117, 5, 0.8)', - 'rgba(144, 19, 254, 0.8)', - 'rgba(238, 169, 184, 0.8)', - 'rgba(224, 187, 228, 0.8)', - 'rgba(225, 223, 211, 0.8)', - 'rgba(255, 255, 255, 0.8)', - 'rgba(155, 155, 155, 0.8)', - 'rgba(0, 0, 0, 0.8)', - ]; + private _commentRef = React.createRef<HTMLDivElement>(); + private _cropRef = React.createRef<HTMLDivElement>(); @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)'; @observable public Status: 'marquee' | 'annotation' | '' = ''; // GPT additions - @observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY; @observable private selectedText: string = ''; - @observable private editorView?: EditorView; - @observable private textDoc?: Doc; - @observable private highlightRange: number[] | undefined; - private selectionRange: number[] | undefined; - - @action - setGPTMode = (mode: GPTPopupMode) => { - this.GPTMode = mode; - }; - - @action - setHighlightRange(r: number[] | undefined) { - this.highlightRange = r; - } - - @action - public setSelectedText = (txt: string) => { - this.selectedText = txt; - }; - @action - public setEditorView = (editor: EditorView) => { - this.editorView = editor; - }; - - @action - public setTextDoc = (textDoc: Doc) => { - this.textDoc = textDoc; - }; + public setSelectedText = (txt: string) => (this.selectedText = txt); public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -108,20 +60,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentWillUnmount() { this._disposer?.(); - this._disposer2?.(); } componentDidMount() { - this._disposer2 = reaction( - () => this._opacity, - opacity => {}, - { fireImmediately: true } - ); this._disposer = reaction( () => SelectionManager.Views().slice(), - selected => { - AnchorMenu.Instance.fadeOut(true); - } + selected => AnchorMenu.Instance.fadeOut(true) ); } @@ -132,17 +76,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { gptSummarize = async (e: React.PointerEvent) => { // move this logic to gptpopup, need to implement generate again GPTPopup.Instance.setVisible(true); - this.setHighlightRange(undefined); GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); GPTPopup.Instance.setLoading(true); try { const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY); - if (res) { - GPTPopup.Instance.setText(res); - } else { - GPTPopup.Instance.setText('Something went wrong.'); - } + GPTPopup.Instance.setText(res || 'Something went wrong.'); } catch (err) { console.error(err); } @@ -154,7 +93,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this, e, (e: PointerEvent) => { - this.StartDrag(e, this._commentCont.current!); + this.StartDrag(e, this._commentRef.current!); return true; }, returnFalse, @@ -171,7 +110,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this, e, (e: PointerEvent) => { - this.StartCropDrag(e, this._commentCont.current!); + this.StartCropDrag(e, this._cropRef.current!); return true; }, returnFalse, @@ -193,7 +132,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { tooltip={'Click to Highlight'} onClick={this.highlightClicked} colorPicker={this.highlightColor} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> <ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} /> </Group> @@ -217,44 +156,28 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * all selected text available to summarize but its only supported for pdf and web ATM. * @returns Whether the GPT icon for summarization should appear */ - canSummarize = (): boolean => { - const docs = SelectionManager.Docs(); - if (docs.length > 0) { - return docs.some(doc => doc.type === DocumentType.PDF || doc.type === DocumentType.WEB); - } - return false; - }; - - /** - * Returns whether the selected text can be edited. - * @returns Whether the GPT icon for summarization should appear - */ - canEdit = (): boolean => { - const docs = SelectionManager.Docs(); - if (docs.length > 0) { - return docs.some(doc => doc.type === 'rtf'); - } - return false; - }; + canSummarize = () => SelectionManager.Docs().some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any)); render() { const buttons = this.Status === 'marquee' ? ( <> {this.highlighter} - <IconButton - tooltip="Drag to Place Annotation" // - onPointerDown={this.pointerDown} - icon={<FontAwesomeIcon icon="comment-alt" />} - color={StrCast(Doc.UserDoc().userColor)} - /> + <div ref={this._commentRef}> + <IconButton + tooltip="Drag to Place Annotation" // + onPointerDown={this.pointerDown} + icon={<FontAwesomeIcon icon="comment-alt" />} + color={SettingsManager.userColor} + /> + </div> {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/} {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && ( <IconButton tooltip="Summarize with AI" // onPointerDown={this.gptSummarize} icon={<FontAwesomeIcon icon="comment-dots" size="lg" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( @@ -262,7 +185,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { tooltip="Click to Record Annotation" // onPointerDown={this.audioDown} icon={<FontAwesomeIcon icon="microphone" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} <Popup @@ -270,15 +193,17 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { type={Type.PRIM} icon={<FontAwesomeIcon icon={'search'} />} popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> {AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : ( - <IconButton - tooltip="Click/Drag to create cropped image" // - onPointerDown={this.cropDown} - icon={<FontAwesomeIcon icon="image" />} - color={StrCast(Doc.UserDoc().userColor)} - /> + <div ref={this._cropRef}> + <IconButton + tooltip="Click/Drag to create cropped image" // + onPointerDown={this.cropDown} + icon={<FontAwesomeIcon icon="image" />} + color={SettingsManager.userColor} + /> + </div> )} </> ) : ( @@ -288,7 +213,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { tooltip="Remove Link Anchor" // onPointerDown={this.Delete} icon={<FontAwesomeIcon icon="trash-alt" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} {this.PinToPres !== returnFalse && ( @@ -296,7 +221,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { tooltip="Pin to Presentation" // onPointerDown={this.PinToPres} icon={<FontAwesomeIcon icon="map-pin" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} {this.ShowTargetTrail !== returnFalse && ( @@ -304,7 +229,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { tooltip="Show Linked Trail" // onPointerDown={this.ShowTargetTrail} icon={<FontAwesomeIcon icon="taxi" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} {this.IsTargetToggler !== returnFalse && ( @@ -315,7 +240,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { toggleStatus={this.IsTargetToggler()} onClick={this.MakeTargetToggle} icon={<FontAwesomeIcon icon="thumbtack" />} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} /> )} </> diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 395fbd1ca..2a191477b 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -46,7 +46,7 @@ interface IViewerProps extends FieldViewProps { sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean; loaded?: (nw: number, nh: number, np: number) => void; setPdfViewer: (view: PDFViewer) => void; - anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void); + anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined; } @@ -67,7 +67,7 @@ export class PDFViewer extends React.Component<IViewerProps> { private _pdfViewer: any; private _styleRule: any; // stylesheet rule for making hyperlinks clickable private _retries = 0; // number of times tried to create the PDF viewer - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -375,7 +375,7 @@ export class PDFViewer extends React.Component<IViewerProps> { this._downY = e.clientY; if ((this.props.Document._freeform_scale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) { - this._setPreviewCursor?.(e.clientX, e.clientY, true, false); + this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this.props.Document); } if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this.props.select(false); @@ -464,12 +464,12 @@ export class PDFViewer extends React.Component<IViewerProps> { onClick = (e: React.MouseEvent) => { this._scrollStopper?.(); if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - this._setPreviewCursor(e.clientX, e.clientY, false, false); + this._setPreviewCursor(e.clientX, e.clientY, false, false, this.props.Document); } // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func); @action diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 6103b245c..540c1b5e1 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -1,17 +1,15 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DocumentType } from "../../documents/DocumentTypes"; -// import "./SearchBox.scss"; -import "./IconBar.scss"; +import { DocumentType } from '../../documents/DocumentTypes'; +import './IconBar.scss'; import { IconButton } from './IconButton'; -import "./IconButton.scss"; +import './IconButton.scss'; export interface IconBarProps { setIcons: (icons: string[]) => void; } - @observer export class IconBar extends React.Component<IconBarProps> { public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP]; @@ -32,7 +30,9 @@ export class IconBar extends React.Component<IconBarProps> { } @action.bound - getIcons(): string[] { return this._icons; } + getIcons(): string[] { + return this._icons; + } constructor(props: any) { super(props); @@ -40,29 +40,33 @@ export class IconBar extends React.Component<IconBarProps> { } @action.bound - getList(): string[] { return this.getIcons(); } + getList(): string[] { + return this.getIcons(); + } @action.bound - updateList(newList: string[]) { this.updateIcon(newList); } + updateList(newList: string[]) { + this.updateIcon(newList); + } @action.bound resetSelf = () => { this._resetClicked = true; this.updateList([]); - } + }; @action.bound selectAll = () => { this._selectAllClicked = true; this.updateList(this._allIcons); - } + }; render() { return ( <div className="icon-bar"> - {this._allIcons.map((type: string) => + {this._allIcons.map((type: string) => ( <IconButton key={type.toString()} type={type} /> - )} + ))} </div> ); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 1ceea697a..aa46e57dc 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -19,6 +19,7 @@ import { fetchRecommendations } from '../newlightbox/utils'; import { IRecommendation, Recommendation } from '../newlightbox/components'; import { Colors } from '../global/globalEnums'; import { SettingsManager } from '../../util/SettingsManager'; +import { SearchUtil } from '../../util/SearchUtil'; const DAMPENING_FACTOR = 0.9; const MAX_ITERATIONS = 25; @@ -132,34 +133,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * @param {Doc[]} docs - docs to be searched through recursively * @param {number, Doc => void} func - function to be called on each doc * - * This method iterates through an array of docs and all docs within those docs, calling - * the function func on each doc. - */ - static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { - let newarray: Doc[] = []; - var depth = 0; - const visited: Doc[] = []; - while (docs.length > 0) { - newarray = []; - docs.filter(d => d && !visited.includes(d)).forEach(d => { - visited.push(d); - const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); - const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - data && newarray.push(...DocListCast(data)); - const sidebar = d[fieldKey + '_sidebar']; - sidebar && newarray.push(...DocListCast(sidebar)); - func(depth, d); - }); - docs = newarray; - depth++; - } - } - - /** - * @param {Doc[]} docs - docs to be searched through recursively - * @param {number, Doc => void} func - function to be called on each doc - * * This method iterates asynchronously through an array of docs and all docs within those * docs, calling the function func on each doc. */ @@ -213,74 +186,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @action searchCollection(query: string) { this._selectedResult = undefined; - this._results = SearchBox.staticSearchCollection(CollectionDockingView.Instance?.rootDoc, query); + this._results = SearchUtil.SearchCollection(CollectionDockingView.Instance?.rootDoc, query); this.computePageRanks(); } - @action - static staticSearchCollection(rootDoc: Opt<Doc>, query: string) { - const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.SEARCH, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - const blockedKeys = [ - 'x', - 'y', - 'proto', - 'width', - 'layout_autoHeight', - 'acl-Override', - 'acl-Guest', - 'embedContainer', - 'zIndex', - 'height', - 'text_scrollHeight', - 'text_height', - 'cloneFieldFilter', - 'isDataDoc', - 'text_annotations', - 'dragFactory_count', - 'text_noTemplate', - 'proto_embeddings', - 'isSystem', - 'layout_fieldKey', - 'isBaseProto', - 'xMargin', - 'yMargin', - 'links', - 'layout', - 'layout_keyValue', - 'layout_fitWidth', - 'type_collection', - 'title_custom', - 'freeform_panX', - 'freeform_panY', - 'freeform_scale', - ]; - query = query.toLowerCase(); - - const results = new Map<Doc, string[]>(); - if (rootDoc) { - const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]); - const docIDs: String[] = []; - SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { - const dtype = StrCast(doc.type) as DocumentType; - if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { - const hlights = new Set<string>(); - SearchBox.documentKeys(doc).forEach( - key => - Field.toString(doc[key] as Field) - .toLowerCase() - .includes(query) && hlights.add(key) - ); - blockedKeys.forEach(key => hlights.delete(key)); - - if (Array.from(hlights.keys()).length > 0) { - results.set(doc, Array.from(hlights.keys())); - } - } - docIDs.push(doc[Id]); - }); - } - return results; - } /** * This method initializes the page rank of every document to the reciprocal @@ -374,17 +283,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } /** - * @param {Doc} doc - doc for which keys are returned - * - * This method returns a list of a document doc's keys. - */ - static documentKeys(doc: Doc) { - const keys: { [key: string]: boolean } = {}; - Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false))); - return Array.from(Object.keys(keys)); - } - - /** * This method submits a search with the _searchString as its query and updates * the results array accordingly. */ @@ -518,7 +416,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { className={className}> <div className="searchBox-result-title">{title as string}</div> <div className="searchBox-result-type">{formattedType}</div> - <div className="searchBox-result-keys" style={{ color: SettingsManager.Instance.userVariantColor }}> + <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}> {result[1].join(', ')} </div> </div> @@ -530,7 +428,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />); return ( - <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.Instance.userColor, background: SettingsManager.Instance.userBackgroundColor }}> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> <div className="searchBox-bar"> {isLinkSearch ? null : ( <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}> @@ -555,7 +453,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { </div> {resultsJSX.length > 0 && ( <div className="searchBox-results-container"> - <div className="section-header" style={{ background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE) }}> + <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> @@ -564,7 +462,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { )} {recommendationsJSX.length > 0 && ( <div className="searchBox-recommendations-container"> - <div className="section-header" style={{ background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE) }}> + <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> diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx index 955a4a174..2139919e0 100644 --- a/src/client/views/selectedDoc/SelectedDocView.tsx +++ b/src/client/views/selectedDoc/SelectedDocView.tsx @@ -1,12 +1,14 @@ import React = require('react'); -import { Doc } from "../../../fields/Doc"; -import { observer } from "mobx-react"; -import { computed } from "mobx"; -import { StrCast } from "../../../fields/Types"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Colors, ListBox } from 'browndash-components'; +import { ListBox } from 'browndash-components'; +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../fields/Doc'; +import { StrCast } from '../../../fields/Types'; import { DocumentManager } from '../../util/DocumentManager'; import { DocFocusOptions } from '../nodes/DocumentView'; +import { emptyFunction } from '../../../Utils'; +import { SettingsManager } from '../../util/SettingsManager'; export interface SelectedDocViewProps { selectedDocs: Doc[]; @@ -14,34 +16,33 @@ export interface SelectedDocViewProps { @observer export class SelectedDocView extends React.Component<SelectedDocViewProps> { - @computed get selectedDocs() { return this.props.selectedDocs; } - render() { - return <div className={`selectedDocView-container`}> - <ListBox - items={this.selectedDocs.map((doc) => { - const icon = Doc.toIcon(doc); - const iconEle = <FontAwesomeIcon size={'1x'} icon={icon} />; - const text = StrCast(doc.title) - const finished = () => { - - }; - const options: DocFocusOptions = { - playAudio: false, - }; - return { - text: text, - val: StrCast(doc._id), - icon: iconEle, - onClick: () => {DocumentManager.Instance.showDocument(doc, options, finished);} - } - })} - color={StrCast(Doc.UserDoc().userColor)} - /> - </div> + return ( + <div className="selectedDocView-container"> + <ListBox + items={this.selectedDocs.map(doc => { + const options: DocFocusOptions = { + playAudio: false, + playMedia: false, + willPan: true, + }; + return { + text: StrCast(doc.title), + val: StrCast(doc._id), + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + icon: <FontAwesomeIcon size={'1x'} icon={Doc.toIcon(doc)} />, + onClick: () => DocumentManager.Instance.showDocument(doc, options, emptyFunction), + }; + })} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + /> + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index c194ede32..55d94406a 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,27 +1,30 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Size, Type, isDark } from 'browndash-components'; +import { Button, IconButton, isDark, Size, Type } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; +import { FaBug } from 'react-icons/fa'; import { Doc, DocListCast } from '../../../fields/Doc'; import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; import { PingManager } from '../../util/PingManager'; import { ReportManager } from '../../util/reportManager/ReportManager'; import { ServerStats } from '../../util/ServerStats'; import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; -import { UndoManager } from '../../util/UndoManager'; +import { Transform } from '../../util/Transform'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; +import { CollectionLinearView } from '../collections/collectionLinear'; import { ContextMenu } from '../ContextMenu'; import { DashboardView } from '../DashboardView'; -import { MainView } from '../MainView'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; import { Colors } from '../global/globalEnums'; +import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { DefaultStyleProvider } from '../StyleProvider'; import './TopBar.scss'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user @@ -37,13 +40,13 @@ export class TopBar extends React.Component { }; @computed get color() { - return StrCast(Doc.UserDoc().userColor, Colors.LIGHT_GRAY); + return SettingsManager.userColor; } @computed get variantColor() { - return StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE); + return SettingsManager.userVariantColor; } @computed get backgroundColor() { - return PingManager.Instance.IsBeating ? SettingsManager.Instance.userBackgroundColor : Colors.MEDIUM_GRAY; + return PingManager.Instance.IsBeating ? SettingsManager.userBackgroundColor : Colors.MEDIUM_GRAY; } @observable happyHeart: boolean = PingManager.Instance.IsBeating; @@ -77,12 +80,57 @@ export class TopBar extends React.Component { </div> )} {Doc.ActiveDashboard && ( - <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.color} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} /> + <Button + text="Explore" + type={Type.TERT} + tooltip="Browsing mode for directly navigating to documents" + size={Size.SMALL} + color={DocumentView.ExploreMode ? this.variantColor : this.color} + background={DocumentView.ExploreMode ? this.color : 'transparent'} + onClick={action(() => (DocumentView.ExploreMode = !DocumentView.ExploreMode))} + /> )} </div> ); } + @computed get dashMenuButtons() { + const selDoc = Doc.MyTopBarBtns; + return !(selDoc instanceof Doc) ? null : ( + <div className="collectionMenu-contMenuButtons" style={{ height: '100%' }}> + <CollectionLinearView + Document={selDoc} + DataDoc={undefined} + fieldKey="data" + dropAction="embed" + setHeight={returnFalse} + styleProvider={DefaultStyleProvider} + rootSelected={returnTrue} + bringToFront={emptyFunction} + select={emptyFunction} + isContentActive={returnTrue} + isAnyChildContentActive={returnFalse} + isSelected={returnFalse} + docViewPath={returnEmptyDoclist} + moveDocument={returnFalse} + addDocument={returnFalse} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={emptyFunction} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 200} + PanelHeight={() => 30} + renderDepth={0} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + </div> + ); + } + /** * Returns the center of the topbar * This part of the topbar contains everything related to the current dashboard including: @@ -110,23 +158,10 @@ export class TopBar extends React.Component { style={{ fontWeight: 700, fontSize: '1rem' }} onClick={(e: React.MouseEvent) => { const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard); - ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' }); dashView?.showContextMenu(e.clientX + 20, e.clientY + 30); }} /> - {!Doc.noviceMode && ( - <IconButton - tooltip="Work on a copy of the dashboard layout" - size={Size.SMALL} - color={this.color} - onClick={async () => { - const batch = UndoManager.StartBatch('snapshot'); - await DashboardView.snapshotDashboard(); - batch.end(); - }} - icon={<FaCamera />} - /> - )} + {!Doc.noviceMode && this.dashMenuButtons} </div> ) : null; } @@ -144,7 +179,8 @@ export class TopBar extends React.Component { <Button text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'} type={Type.TERT} - color={this.variantColor} + color={SettingsManager.userColor} + background={this.variantColor} onClick={() => { SharingManager.Instance.open(undefined, Doc.ActiveDashboard); }} diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 02e44a793..524492226 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -6,8 +6,8 @@ import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import '../../views/nodes/WebBox.scss'; -import { DocumentDecorations } from '../DocumentDecorations'; import { CollectionFreeFormDocumentViewProps } from '../nodes/CollectionFreeFormDocumentView'; +import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './DashWebRTCVideo.scss'; import { hangup, initialize, refreshVideos } from './WebCamLogic'; @@ -71,8 +71,8 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV </div> ); - const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? '-interactive' : ''); + const frozen = !this.props.isSelected() || DocumentView.Interacting; + const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : ''); return ( <> |
