diff options
Diffstat (limited to 'src/client/views')
218 files changed, 7677 insertions, 6376 deletions
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 5e39387b8..5fa69f28c 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index 8a0e5480e..b205a0f1e 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -5,11 +5,15 @@ position: absolute; z-index: 10001; height: $antimodemenu-height; - background: $dark-gray; - border-bottom: $standard-border; + width: fit-content; + border-radius: $standard-border-radius; + overflow: hidden; // box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); // border-radius: 0px 6px 6px 6px; display: flex; + justify-content: center; + align-items: center; + gap: 3px; &.with-rows { flex-direction: column @@ -20,30 +24,6 @@ height: 35px; } - .antimodeMenu-button { - background-color: transparent; - width: 35px; - height: 35px; - padding: 5; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - position: relative; - - .svg { - margin: 0; - } - - &.active { - background-color: #121212; - } - } - - .antimodeMenu-button:hover { - background-color: rgba(0, 0, 0, 0.4); - } - .antimodeMenu-dragger { height: 100%; transition: width .2s; diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index de1207ce4..c41ea7053 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -1,6 +1,8 @@ import React = require('react'); import { observable, action, runInAction } from 'mobx'; import './AntimodeMenu.scss'; +import { StrCast } from '../../fields/Types'; +import { Doc } from '../../fields/Doc'; export interface AntimodeMenuProps {} /** @@ -148,6 +150,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), transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, @@ -173,6 +176,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co height: 'inherit', width: 200, opacity: this._opacity, + background: StrCast(Doc.UserDoc().userBackgroundColor), transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, @@ -195,6 +199,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), transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 14c922526..c779ce8c4 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -3,7 +3,7 @@ import axios from 'axios'; import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import Waveform from 'react-audio-waveform'; -import { Doc } from '../../fields/Doc'; +import { Doc, NumListCast } from '../../fields/Doc'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; import { Cast } from '../../fields/Types'; @@ -54,7 +54,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { } @computed get audioBuckets() { - return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec('number'), []); + return NumListCast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]); } audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10; @@ -102,7 +102,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { render() { return ( <div className="audioWaveform"> - <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} progressColor={Colors.MEDIUM_BLUE_ALT} /> + <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={Array.from(this.audioBuckets)} progressColor={Colors.MEDIUM_BLUE_ALT} /> </div> ); } diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index cbe14060a..588eff1d1 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -7,6 +7,7 @@ box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); flex-direction: column; background: whitesmoke; + color: black; border-radius: 3px; } @@ -51,13 +52,29 @@ user-select: none; transition: all 0.1s; border-style: none; - // padding: 10px 0px 10px 0px; + position: relative; white-space: nowrap; font-size: 13px; letter-spacing: 2px; text-transform: uppercase; padding-right: 30px; + .contextMenu-item-background { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 0; + filter: opacity(0); + } + + &:hover { + .contextMenu-item-background { + filter: opacity(0.2) !important; + } + } + .contextMenu-item-icon-background { pointer-events: all; background-color: transparent; @@ -132,11 +149,6 @@ // border-top: solid 1px; //TODO:glr clean } -.contextMenu-item:hover { - transition: all 0.1s ease; - background: $light-blue; -} - .contextMenu-description { margin-left: 5px; text-align: left; diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index e4c3e864b..8412a9aae 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -5,6 +5,8 @@ import { observer } from 'mobx-react'; import './ContextMenu.scss'; import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextMenuItem'; import { Utils } from '../../Utils'; +import { StrCast } from '../../fields/Types'; +import { Doc } from '../../fields/Doc'; @observer export class ContextMenu extends React.Component { @@ -190,7 +192,11 @@ export class ContextMenu extends React.Component { } return this.filteredItems.map((value, index) => Array.isArray(value) ? ( - <div className="contextMenu-group"> + <div + className="contextMenu-group" + style={{ + background: StrCast(Doc.UserDoc().userVariantColor), + }}> <div className="contextMenu-description">{value.join(' -> ')}</div> </div> ) : ( @@ -213,13 +219,18 @@ export class ContextMenu extends React.Component { this._height = Number(getComputedStyle(r).height.replace('px', '')); } })} - style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}> + style={{ + left: this.pageX, + ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }), + background: StrCast(Doc.UserDoc().userBackgroundColor), + color: StrCast(Doc.UserDoc().userColor), + }}> {!this.itemsNeedSearch ? null : ( <span className={'search-icon'}> <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + <input style={{ color: 'black' }} className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> )} {this.menuItems} diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index e87d2046b..daa2c152a 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -4,6 +4,8 @@ import { observer } from 'mobx-react'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UndoManager } from '../util/UndoManager'; +import { Doc } from '../../fields/Doc'; +import { StrCast } from '../../fields/Types'; export interface OriginalMenuProps { description: string; @@ -40,10 +42,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { if ('event' in this.props) { this.props.closeMenu?.(); - let batch: UndoManager.Batch | undefined; - if (this.props.undoable !== false) { - batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); - } + const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined; await this.props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } @@ -93,6 +92,11 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select </span> ) : null} <div className="contextMenu-description">{this.props.description.replace(':', '')}</div> + <div className={`contextMenu-item-background`} + style={{ + background: StrCast(Doc.UserDoc().userColor) + }} + /> </div> ); } else if ('subitems' in this.props) { @@ -106,6 +110,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select style={{ marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%', marginTop, + background: StrCast(Doc.UserDoc().userBackgroundColor) }}> {this._items.map(prop => ( <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> @@ -136,6 +141,11 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select {this.props.description} <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> + <div className={`contextMenu-item-background`} + style={{ + background: StrCast(Doc.UserDoc().userColor) + }} + /> {submenu} </div> ); diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss index b8a6f6c05..6be2133ef 100644 --- a/src/client/views/DashboardView.scss +++ b/src/client/views/DashboardView.scss @@ -1,12 +1,15 @@ -@import "./global/globalCssVariables"; - +@import './global/globalCssVariables'; .dashboard-view { - padding: 50px; - display: flex; - flex-direction: row; - width: 100%; - position: absolute; + padding: 50px; + display: flex; + flex-direction: row; + width: 100%; + position: absolute; + height: 100%; + width:100%; + padding-right: 0px; + overflow: auto; .left-menu { display: flex; @@ -14,26 +17,28 @@ flex-direction: column; width: 250px; min-width: 250px; + gap: 5px; } - .all-dashboards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - overflow-y: scroll; - } + .all-dashboards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: auto; + width: 100%; + } } .text-button { cursor: pointer; - padding: 3px 0; - &:hover { - font-weight: 500; - } + padding: 3px 0; + &:hover { + font-weight: 500; + } - &.selected { - font-weight: 700; - } + &.selected { + font-weight: 700; + } } .new-dashboard-button { @@ -56,15 +61,26 @@ display: flex; justify-content: center; align-items: center; + position: relative; &:hover { color: $light-blue; border: solid 2px $light-blue; } + + .background { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: -1; + } } .dashboard-container { border-radius: 10px; + position: relative; cursor: pointer; width: 250px; height: 200px; @@ -74,35 +90,55 @@ margin: 0 0px 30px 30px; overflow: hidden; - &:hover{ + &:hover { outline: solid 2px $light-blue; - } + } - .title { - margin: 10px; - font-weight: 500; - } + .title { + margin: 10px; + font-weight: 500; + } - img { - width: auto; - height: 80%; - } + img { + width: auto; + height: 80%; + } - .info { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 0px 10px; - } + .info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px 10px; + } + .dashboard-status, + .dashboard-status-shared { + font-size: 9; + left: 10%; + position: relative; + top: -5; + } + .dashboard-status-shared { + background: 'lightgreen'; + } .more { z-index: 100; } + + .background { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: -1; + } } .new-dashboard { color: $dark-gray; + padding: 10px; display: flex; width: 100%; height: 100%; @@ -136,4 +172,4 @@ flex-direction: row; justify-content: flex-end; } -}
\ No newline at end of file +} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 02c5085a2..21808d6e0 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,15 +1,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-components'; +import { Button, ColorPicker, EditableText, FontSize, IconButton, Size, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DataSym, Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; +import { FaPlus } from 'react-icons/fa'; +import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; +import { AclPrivate, DocAcl } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { listSpec } from '../../fields/Schema'; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, DocCast, ImageCast, StrCast } from '../../fields/Types'; +import { Cast, ImageCast, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { CollectionViewType } from '../documents/DocumentTypes'; @@ -23,8 +25,7 @@ import { ContextMenu } from './ContextMenu'; import './DashboardView.scss'; import { Colors } from './global/globalEnums'; import { MainViewModal } from './MainViewModal'; -import { ButtonType } from './nodes/button/FontIconBox'; -import { FaPlus } from 'react-icons/fa'; +import { ButtonType } from './nodes/FontIconBox/FontIconBox'; enum DashboardGroup { MyDashboards, @@ -42,13 +43,16 @@ export class DashboardView extends React.Component { @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; @observable private newDashboardName: string | undefined = undefined; - @observable private newDashboardColor: 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) => { @@ -56,28 +60,30 @@ export class DashboardView extends React.Component { }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { - Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); - Doc.ActiveDashboard = dashboard; + if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { + DashboardView.openSharedDashboard(dashboard); + } else { + Doc.ActiveDashboard = dashboard; + } Doc.ActivePage = 'dashboard'; }; - getDashboards = () => { + getDashboards = (whichGroup: DashboardGroup) => { const allDashboards = DocListCast(Doc.MyDashboards.data); - if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) { + if (whichGroup === DashboardGroup.MyDashboards) { return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); - } else { - const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking); - return sharedDashboards; } + const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); + return sharedDashboards; }; isUnviewedSharedDashboard = (dashboard: Doc): boolean => { - // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking); + // 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).filter(doc => doc._viewType === CollectionViewType.Docking); + const sharedDashs = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking); return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard)); }; @@ -94,23 +100,18 @@ export class DashboardView extends React.Component { const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1; const placeholder = `Dashboard ${dashboardCount}`; return ( - <div className="new-dashboard"> + <div + className="new-dashboard" + style={{ + background: StrCast(Doc.UserDoc().userBackgroundColor), + color: StrCast(Doc.UserDoc().userColor), + }}> <div className="header">Create New Dashboard</div> - <div className="title-input"> - Title - <input className="input" placeholder={placeholder} onChange={e => this.setNewDashboardName((e.target as any).value)} /> - </div> - <div className="color-picker"> - Background - <ColorPicker - onChange={color => { - this.newDashboardColor = color; - }} - /> - </div> + <EditableText formLabel="Title" placeholder={placeholder} type={Type.SEC} color={StrCast(Doc.UserDoc().userColor)} setVal={val => this.setNewDashboardName(val as string)} fillWidth /> + <ColorPicker formLabel="Background" colorPickerType="github" type={Type.TERT} selectedColor={this.newDashboardColor} setSelectedColor={this.setNewDashboardColor} /> <div className="button-bar"> - <Button text="Cancel" onClick={this.abortCreateNewDashboard} /> - <Button text="Create" onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} /> + <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)} /> </div> </div> ); @@ -149,32 +150,43 @@ export class DashboardView extends React.Component { }; render() { + const color = StrCast(Doc.UserDoc().userColor); + const variant = StrCast(Doc.UserDoc().userVariantColor); return ( <> <div className="dashboard-view"> <div className="left-menu"> - <div className="new-dashboard-button"> - <Button icon={<FaPlus />} size={Size.MEDIUM} text="New" onClick={() => this.setNewDashboardName('')} /> - </div> - <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}> - My Dashboards - </div> - <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}> - Shared Dashboards - </div> + <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'} + onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)} + fillWidth + /> + <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={() => this.setNewDashboardName('')} /> </div> <div className="all-dashboards"> - {this.getDashboards().map(dashboard => { + {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]} onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}> + <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"> - <input style={{ border: 'unset' }} className="input" onClick={e => e.stopPropagation()} defaultValue={StrCast(dashboard.title)} onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} /> + <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" @@ -187,9 +199,17 @@ export class DashboardView extends React.Component { e.stopPropagation(); this.onContextMenu(dashboard, e); }}> - <Button size={Size.SMALL} icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} /> + <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> ); })} @@ -199,6 +219,13 @@ export class DashboardView extends React.Component { this.setNewDashboardName(''); }}> + + <div + className={`background`} + style={{ + background: StrCast(Doc.UserDoc().userColor), + filter: 'opacity(0.2)', + }} + /> </div> </div> </div> @@ -220,6 +247,11 @@ export class DashboardView extends React.Component { return CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard); } + public static openSharedDashboard = (dashboard: Doc) => { + Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); + DashboardView.openDashboard(Doc.BestEmbedding(dashboard)); + }; + /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there). /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { @@ -346,13 +378,13 @@ export class DashboardView extends React.Component { }, ], }; - Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true); + if (dashboard.dockingConfig && dashboard.dockingConfig !== Doc.GetProto(dashboard).dockingConfig) dashboard.dockingConfig = JSON.stringify(reset); + else Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true); return reset; }; public static createNewDashboard = (id?: string, name?: string, background?: string) => { - const dashboards = Doc.MyDashboards; - const dashboardCount = DocListCast(dashboards.data).length + 1; + const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, @@ -366,14 +398,10 @@ export class DashboardView extends React.Component { 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'); - freeformDoc.embedContainer = dashboardDoc; - // switching the tabs from the datadoc to the regular doc - const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); - dashboardDoc.data = new List<Doc>(dashboardTabs); dashboardDoc['pane-count'] = 1; - Doc.AddDocToList(dashboards, 'data', dashboardDoc); + Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); DashboardView.SetupDashboardTrails(dashboardDoc); @@ -389,10 +417,11 @@ export class DashboardView extends React.Component { _forceActive: true, _width: 30, _height: 30, - _stayInCollection: true, - _hideContextMenu: true, + _dragOnlyWithinContainer: true, + _layout_hideContextMenu: true, title: 'New trail', toolTip: 'Create new trail', + color: Colors.BLACK, btnType: ButtonType.ClickButton, buttonText: 'New trail', icon: 'plus', @@ -412,19 +441,18 @@ export class DashboardView extends React.Component { _layout_fitWidth: true, _gridGap: 5, _forceActive: true, - childDropAction: 'embed', + childDragAction: 'embed', treeViewTruncateTitleWidth: 150, ignoreClick: true, - buttonMenu: true, - buttonMenuDoc: myTrailsBtn, + layout_headerButton: myTrailsBtn, contextMenuIcons: new List<string>(['plus']), contextMenuLabels: new List<string>(['Create New Trail']), _lockedPosition: true, - boxShadow: '0 0', + layout_boxShadow: '0 0', childDontRegisterViews: true, - targetDropAction: 'same', + dropAction: 'same', isSystem: true, - explainer: 'All of the trails that you have created will appear here.', + layout_explainer: 'All of the trails that you have created will appear here.', }; const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeViewChildDoubleClick: 'openPresentation(documentView.rootDoc)' }); dashboardDoc.myTrails = new PrefetchProxy(myTrails); @@ -436,10 +464,6 @@ export class DashboardView extends React.Component { } } -export function AddToList(MySharedDocs: Doc, arg1: string, dash: any) { - throw new Error('Function not implemented.'); -} - ScriptingGlobals.add(function createNewDashboard() { return DashboardView.createNewDashboard(); }, 'creates a new dashboard when called'); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index ffb2d92f6..e076e69ca 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,15 +1,14 @@ import { action, computed, observable } from 'mobx'; import { DateField } from '../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Opt } from '../../fields/Doc'; -import { InkTool } from '../../fields/InkField'; +import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { Cast, ScriptCast } from '../../fields/Types'; -import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; +import { Cast, DocCast, StrCast } from '../../fields/Types'; +import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { InteractionUtils } from '../util/InteractionUtils'; -import { UndoManager } from '../util/UndoManager'; import { DocumentView } from './nodes/DocumentView'; import { Touchable } from './Touchable'; @@ -36,7 +35,7 @@ export function DocComponent<P extends DocComponentProps>() { } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.Document[DataSym] as Doc; + return this.props.Document[DocData] as Doc; } // key where data is stored @computed get fieldKey() { @@ -74,7 +73,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() { } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; + return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; } // key where data is stored @computed get fieldKey() { @@ -118,7 +117,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; + return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; } // key where data is stored @@ -128,8 +127,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() isAnyChildContentActive = () => this._isAnyChildContentActive; - lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @computed public get annotationKey() { @@ -148,13 +145,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Doc.MyRecentlyClosed; + const recent = this.rootDoc !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); + Doc.RemoveDocFromList(Doc.GetProto(doc), 'proto_embeddings', doc); doc.embedContainer = undefined; if (recent) { - Doc.RemoveDocFromList(recent, 'data', doc); doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); } }); @@ -174,8 +171,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() return true; } const first = doc instanceof Doc ? doc : doc[0]; - if (!first?._stayInCollection && addDocument !== returnFalse) { - return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey)); + if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) { + return this.removeDocument(doc, annotationKey, false) && addDocument(doc, annotationKey); } return false; }; @@ -185,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { return false; } - const targetDataDoc = this.props.Document[DataSym]; + const targetDataDoc = this.rootDoc[DocData]; const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -193,36 +190,16 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } const added = docs; if (added.length) { - const aclKeys = Object.keys(this.props.Document[AclSym] ?? {}); - aclKeys.forEach(key => - added.forEach(d => { - if (d.author === denormalizeEmail(key.substring(4)) && !d.createdFrom) { - distributeAcls(key, SharingPermissions.Admin, d); - } - }) - ); - - if (effectiveAcl === AclAugment) { - added.map(doc => { - if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); - doc.embedContainer = this.props.Document; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; - Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); + if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) { + added.forEach(doc => { + doc._dragOnlyWithinContainer = undefined; + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + Doc.SetContainer(doc, this.rootDoc); + inheritParentAcls(targetDataDoc, doc, true); }); - } else { - added - .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) - .map(doc => { - // only make a pushpin if we have acl's to edit the document - //DocUtils.LeavePushpin(doc); - doc._stayInCollection = undefined; - doc.embedContainer = this.props.Document; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - - Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc); - }); + const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; - if (annoDocs instanceof List) annoDocs.push(...added); + 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())); } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 547d844ca..6f5e9f5c0 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -464,6 +464,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV action(() => { this._isRecording = false; this._stopFunc(); + b.end(); }), emptyFunction ); @@ -588,8 +589,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div style={{ position: 'absolute', zIndex: 1000 }}> <LinkPopup key="popup" - showPopup={this._showLinkPopup} - linkCreated={link => (link.layout_linkDisplay = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))} + linkCreated={link => (link.link_displayLine = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))} linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this.props.views().lastElement()?.rootDoc} /> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ccac5ffe4..ca3610cc0 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -112,6 +112,33 @@ $resizeHandler: 8px; } } + .documentDecorations-lockButton { + display: flex; + align-items: center; + justify-content: center; + background: grey; + border: solid 1.5px rgb(72, 71, 71); + color: grey; + transition: 0.1s ease; + opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: rgb(72, 71, 71); + opacity: 1; + } + + > svg { + margin: 0; + } + } + .documentDecorations-minimizeButton { display: flex; align-items: center; @@ -152,6 +179,7 @@ $resizeHandler: 8px; display: flex; height: 20px; border-radius: 8px; + gap: 2px; outline: none; border: none; opacity: 0.3; @@ -186,6 +214,79 @@ $resizeHandler: 8px; } } + .documentDecorations-share { + background: none; + opacity: 1; + grid-column: 3; + pointer-events: auto; + min-width: fit-content; + text-align: center; + display: flex; + height: 21px; + opacity: 0.3; + &:hover { + opacity: 1; + } + + + .checkbox{ + display: inline; + + .checkbox-box{ + display: inline; + position: relative; + top: -2.5; + left: 35; + zoom: .7; + } + + & .checkbox-text{ + display: inline; + position: relative; + top: 1.5; + font-size: 8px; + } + } + + .documentDecorations-shareNone{ + width: calc(100% + 10px); + background: grey; + color: rgb(71, 71, 71); + border-radius: 8px; + border: 2px solid rgb(71, 71, 71); + } + .documentDecorations-shareEdit, + .documentDecorations-shareAdmin{ + width: calc(100% + 10px); + background: rgb(254, 254, 199); + color: rgb(75, 75, 5); + border-radius: 8px; + border: 2px solid rgb(75, 75, 5); + } + .documentDecorations-shareAugment{ + width: calc(100% + 10px); + background: rgb(208, 255, 208); + color:rgb(19, 80, 19); + border-radius: 8px; + border: 2px solid rgb(19, 80, 19); + + } + .documentDecorations-shareView{ + width: calc(100% + 10px); + background: rgb(213, 213, 255); + color: rgb(25, 25, 101); + border-radius: 8px; + border: 2px solid rgb(25, 25, 101); + } + .documentDecorations-shareNot-Shared{ + width: calc(100% + 10px); + background: rgb(255, 207, 207); + color: rgb(146, 58, 58); + border-radius: 8px; + border: 2px solid rgb(146, 58, 58); + } + } + .documentDecorations-centerCont { grid-column: 2; background: none; @@ -264,7 +365,7 @@ $resizeHandler: 8px; .documentDecorations-lock { position: relative; background: black; - color: gray; + color: rgb(145, 144, 144); height: 14; width: 14; pointer-events: all; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 39073d763..f3daf3ffa 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -2,23 +2,26 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { IconButton } from 'browndash-components'; -import { action, computed, observable, reaction, runInAction } from 'mobx'; +import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; import { DateField } from '../../fields/DateField'; -import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; -import { Document } from '../../fields/documentSchemas'; +import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols'; import { InkField } from '../../fields/InkField'; +import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; +import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; +import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; @@ -27,15 +30,11 @@ import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; +import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); -import { RichTextField } from '../../fields/RichTextField'; -import { LinkFollower } from '../util/LinkFollower'; import _ = require('lodash'); -import { DocumentManager } from '../util/DocumentManager'; -import { isUndefined } from 'lodash'; @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> { @@ -65,6 +64,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @observable private _isRotating: boolean = false; @observable private _isRounding: boolean = false; @observable private _isResizing: boolean = false; + @observable private showLayoutAcl: boolean = false; constructor(props: any) { super(props); @@ -150,7 +150,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); } }), - 'title blur' + 'edit title' ); } }; @@ -163,35 +163,50 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }; @action onContainerDown = (e: React.PointerEvent): void => { - setupMoveUpEvents( - this, - e, - e => this.onBackgroundMove(true, e), - e => {}, - emptyFunction - ); + const first = SelectionManager.Views()[0]; + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + emptyFunction + ); + } }; @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents( - this, - e, - e => this.onBackgroundMove(true, e), - e => {}, - action(e => { - !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); - this._editingTitle = true; - this._keyinput.current && setTimeout(this._keyinput.current.focus); - }) - ); + const first = SelectionManager.Views()[0]; + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + action(e => { + !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); + this._editingTitle = true; + this._keyinput.current && setTimeout(this._keyinput.current.focus); + }) + ); + } }; onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { + const first = SelectionManager.Views()[0]; + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) { + return false; + } const dragDocView = SelectionManager.Views()[0]; - if (DocListCast(Doc.MyOverlayDocs.data).includes(dragDocView.rootDoc)) return false; + const containers = new Set<Doc | undefined>(); + SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer))); + if (containers.size > 1) return false; const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData( SelectionManager.Views().map(dv => dv.props.Document), @@ -227,11 +242,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P .filter(v => v && v.props.renderDepth > 0); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; - if (!this._iconifyBatch) { - this._iconifyBatch = UndoManager.StartBatch('iconifying'); - } else { - forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes - } var iconifyingCount = views.length; const finished = action((force?: boolean) => { if ((force || --iconifyingCount === 0) && this._iconifyBatch) { @@ -250,6 +260,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P this._iconifyBatch = undefined; } }); + if (!this._iconifyBatch) { + 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 + } + if (forceDeleteOrIconify) finished(forceDeleteOrIconify); else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); }; @@ -273,19 +289,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (selectedDocs.length) { if (e.ctrlKey) { // open an embedding in a new tab with Ctrl Key - CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].props.Document), OpenWhereMod.right); + CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key - const embedding = Doc.MakeEmbedding(selectedDocs[0].props.Document); + const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc); embedding.embedContainer = undefined; - embedding.x = -embedding[WidthSym]() / 2; - embedding.y = -embedding[HeightSym]() / 2; + embedding.x = -embedding[Width]() / 2; + embedding.y = -embedding[Height]() / 2; CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right); + CollectionDockingView.ToggleSplit(selectedDocs[0].rootDoc, OpenWhereMod.right); } else { - var openDoc = selectedDocs[0].props.Document; + var openDoc = selectedDocs[0].rootDoc; if (openDoc.layout_fieldKey === 'layout_icon') { openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); Doc.deiconifyView(openDoc); @@ -293,7 +309,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P LightboxView.SetLightboxDoc( openDoc, undefined, - selectedDocs.slice(1).map(view => view.props.Document) + selectedDocs.slice(1).map(view => view.rootDoc) ); } } @@ -330,7 +346,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); const ratio = dist / maxDist; const radius = Math.min(1, ratio) * docMax; - doc.borderRounding = `${radius}px`; + doc.layout_borderRounding = `${radius}px`; }); return false; }, // moveEvent @@ -397,7 +413,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; - const rotateUndo = UndoManager.StartBatch('rotatedown'); + const rotateUndo = UndoManager.StartBatch('drag rotation'); const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); const centerPoint = this.rotCenter.slice(); const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>(); @@ -465,7 +481,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX; this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY; - this._resizeUndo = UndoManager.StartBatch('DocDecs resize'); + this._resizeUndo = UndoManager.StartBatch('drag resizing'); this._snapX = e.pageX; this._snapY = e.pageY; const ffviewSet = new Set<CollectionFreeFormView>(); @@ -479,6 +495,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; + const effectiveAcl = GetEffectiveAcl(first.rootDoc); + if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false; if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); @@ -586,7 +604,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let dW = docwidth * (dWin / refWidth); let dH = docheight * (dHin / refHeight); const scale = docView.props.ScreenToLocalTransform().Scale; - const modifyNativeDim = (e.ctrlKey || doc.layout_forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen); + const modifyNativeDim = (e.ctrlKey && doc.nativeDimModifiable) || (doc.layout_forceReflow && !dragBottom && !dragTop) || (doc.nativeHeightUnfrozen && (dragBottom || dragTop || e.ctrlKey)); if (nwidth && nheight) { if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) { docheight = (nheight / nwidth) * docwidth; @@ -599,9 +617,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } let actualdW = Math.max(docwidth + dW * scale, 20); let actualdH = Math.max(docheight + dH * scale, 20); - let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth)); - let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight)); - const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false; + let dX = !dWin ? 0 : (scale * refCent[0] * -dWin) / refWidth; + let dY = !dHin ? 0 : (scale * refCent[1] * -dHin) / refHeight; + const preserveNativeDim = !doc._nativeHeightUnfrozen && !doc._nativeDimModifiable; const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { @@ -612,6 +630,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } else { if (!doc._layout_fitWidth || preserveNativeDim) { actualdH = (nheight / nwidth) * actualdW; + dYin && (dY = -dW * scale * (nheight / nwidth)); doc._height = actualdH; } else if (!modifyNativeDim || dragBotRight) { doc._height = actualdH; @@ -628,13 +647,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } else { if (!doc._layout_fitWidth || preserveNativeDim) { actualdW = (nwidth / nheight) * actualdH; + dXin && (dX = -dH * scale * (nwidth / nheight)); doc._width = actualdW; } else if (!modifyNativeDim || dragBotRight) { doc._width = actualdW; } } if (!modifyNativeDim) { - actualdH = Math.min((nheight / nwidth) * docwidth, actualdH); + actualdH = (nheight / nwidth) * NumCast(doc._width); //, actualdH); } doc._height = actualdH; } @@ -744,17 +764,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setTimeout(action(() => (this._showNothing = true))); return null; } + + // sharing + const acl = GetEffectiveAcl(!this.showLayoutAcl ? Doc.GetProto(seldocview.rootDoc) : seldocview.rootDoc); + const docShareMode = HierarchyMapping.get(acl)!.name; + const shareMode = StrCast(docShareMode); + var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; + // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; - const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; + const hideResizers = + ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; - const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; + const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = hideDecorations || seldocview.props.hideOpenButton || - seldocview.rootDoc.hideOpenButton || - SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) || + seldocview.rootDoc.layout_hideOpenButton || + SelectionManager.Views().some(docView => docView.rootDoc._dragOnlyWithinContainer || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = @@ -764,26 +792,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P seldocview.props.hideDeleteButton || seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { - const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit; - return (docView.rootDoc.stayInCollection && !docView.rootDoc.timelineLabel) || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); + const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DocData]) : AclEdit; + return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin; }); - const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> - <div - className={`documentDecorations-${key}Button`} - onContextMenu={e => e.preventDefault()} - onPointerDown={ - pointerDown ?? - (e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoBatch(e => click!(e)) - )) - }> + <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}> <FontAwesomeIcon icon={icon as any} /> </div> </Tooltip> @@ -807,12 +821,33 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; - const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding)); + const borderRadius = numberValue(StrCast(seldocview.rootDoc.layout_borderRounding)); const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); 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 ? ( + <div className="checkbox"> + <div className="checkbox-box"> + <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} /> + </div> + <div className="checkbox-text"> Layout </div> + </div> + ) : null} + */} + </div> + </div> + ) : ( + <div /> + ); + const titleArea = this._editingTitle ? ( <input ref={this._keyinput} @@ -827,8 +862,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerDown={e => e.stopPropagation()} /> ) : ( - <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> - <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> + <div + className="documentDecorations-title" + key="title" + onPointerDown={e => { + e.stopPropagation; + }}> + {hideTitle ? null : ( + <span className={`documentDecorations-titleSpan${colorScheme}`} onPointerDown={this.onTitleDown}> + {this.selectionTitle} + </span> + )} + {sharingMenu} {!useLock ? null : ( <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top"> <div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}> @@ -838,6 +883,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P )} </div> ); + return ( <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> <div @@ -867,11 +913,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> - <div className="documentDecorations-topbar" style={{ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined }} onPointerDown={this.onContainerDown}> + <div + className="documentDecorations-topbar" + style={{ + color: 'black', + display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined, + }} + onPointerDown={this.onContainerDown}> {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} - {hideTitle ? null : titleArea} - {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as new embedding, shift: in new collection)')} + {titleArea} + {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} </div> {hideResizers ? null : ( <> @@ -927,7 +979,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }}> {this._isRotating ? null : ( <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}> - <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> + <div className="documentDecorations-rotation" style={{ pointerEvents: 'all', color: 'blue' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> <IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} /> </div> </Tooltip> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 6b4132814..d60617020 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -6,6 +6,7 @@ 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 { /** @@ -156,12 +157,14 @@ export class EditableView extends React.Component<EditableProps> { break; case ':': if (this.props.menuCallback) { + e.stopPropagation(); this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; } default: if (this.props.textCallback?.(e.key)) { + e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); } @@ -229,7 +232,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)} @@ -245,23 +248,24 @@ 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() { diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss index c903f29ee..4f0460659 100644 --- a/src/client/views/FilterPanel.scss +++ b/src/client/views/FilterPanel.scss @@ -156,7 +156,7 @@ right: 0; top: 0; z-index: 1; - background-color: #9f9f9f; + // background-color: #9f9f9f; .filterBox-tree { z-index: 0; @@ -182,6 +182,7 @@ } .filterBox-tree { + padding-top: 8px; display: inline-block; width: 100%; margin-bottom: 10px; @@ -189,3 +190,23 @@ overflow: auto; } } + + + +.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; + } + +} + + + diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 75e0e7c4c..63bd01b19 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -10,6 +10,9 @@ import { UserOptions } from '../util/GroupManager'; 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'; interface filterProps { rootDoc: Doc; @@ -31,7 +34,7 @@ export class FilterPanel extends React.Component<filterProps> { return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data'; } @computed get targetDocChildren() { - return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data); + return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])]; } @computed get allDocs() { @@ -63,14 +66,14 @@ export class FilterPanel extends React.Component<filterProps> { * The current attributes selected to filter based on */ @computed get activeFilters() { - return StrListCast(this.targetDoc?._docFilters); + return StrListCast(this.targetDoc?._childFilters); } /** * @returns a string array of the current attributes */ @computed get currentFacets() { - return this.activeFilters.map(filter => filter.split(':')[0]); + return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); } gatherFieldValues(childDocs: Doc[], facetKey: string) { @@ -89,6 +92,7 @@ export class FilterPanel extends React.Component<filterProps> { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView'); DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -106,8 +110,8 @@ export class FilterPanel extends React.Component<filterProps> { @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?._docFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')); - setTimeout(() => StrListCast(this.targetDoc?._docFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')))); + 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; } /** @@ -171,21 +175,31 @@ export class FilterPanel extends React.Component<filterProps> { <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} /> </div> - <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._filterBoolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.filterBoolean)}> + {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} + {/* <div className="filterBox-select-bool"> + <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}> {['AND', 'OR'].map(bool => ( <option value={bool} key={bool}> {bool} </option> ))} </select> - </div>{' '} + </div>{' '} */} </div> <div className="filterBox-tree" key="tree"> {Array.from(this.activeFacets.keys()).map(facetHeader => ( <div> - {facetHeader} + <div className="filterBox-facetHeader"> + <div className="filterBox-facetHeader-Header"> </div> + {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)} + + <div className="filterBox-facetHeader-collapse"> + <AiOutlineMinusSquare /> + {/* <CiCircleRemove/> */} + </div> + </div> + {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)} </div> ))} @@ -200,11 +214,12 @@ export class FilterPanel extends React.Component<filterProps> { return ( <input placeholder={ - StrListCast(this.targetDoc._docFilters) - .find(filter => filter.split(':')[0] === facetHeader) - ?.split(':')[1] ?? '-empty-' + StrListCast(this.targetDoc._childFilters) + .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader) + ?.split(Doc.FilterSep)[1] ?? '-empty-' } - onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')} + 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)} /> ); case 'checkbox': @@ -215,12 +230,12 @@ export class FilterPanel extends React.Component<filterProps> { <input style={{ width: 20, marginLeft: 20 }} checked={ - StrListCast(this.targetDoc._docFilters) - .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue) - ?.split(':')[2] === 'check' + StrListCast(this.targetDoc._childFilters) + .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue) + ?.split(Doc.FilterSep)[2] === 'check' } type={type} - onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')} + onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue} </div> diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 141e99c66..35d6d73e4 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -2,20 +2,19 @@ import React = require('react'); import * as fitCurve from 'fit-curve'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { Doc, Opt } from '../../fields/Doc'; import { InkData, InkTool } from '../../fields/InkField'; -import { ScriptField } from '../../fields/ScriptField'; -import { Cast, FieldValue, NumCast } from '../../fields/Types'; +import { NumCast } from '../../fields/Types'; import MobileInkOverlay from '../../mobile/MobileInkOverlay'; import { GestureUtils } from '../../pen-gestures/GestureUtils'; import { MobileInkOverlayContent } from '../../server/Message'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { CognitiveServices } from '../cognitive_services/CognitiveServices'; -import { Docs, DocUtils } from '../documents/Documents'; import { InteractionUtils } from '../util/InteractionUtils'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { Transform } from '../util/Transform'; import './GestureOverlay.scss'; +import { InkTranscription } from './InkTranscription'; import { ActiveArrowEnd, ActiveArrowScale, @@ -31,13 +30,11 @@ import { SetActiveInkColor, SetActiveInkWidth, } from './InkingStroke'; -import { InkTranscription } from './InkTranscription'; -import { checkInksToGroup } from './nodes/button/FontIconBox'; +import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu'; +import { Touchable } from './Touchable'; +import { checkInksToGroup } from './global/globalScripts'; import { DocumentView } from './nodes/DocumentView'; import { RadialMenu } from './nodes/RadialMenu'; -import HorizontalPalette from './Palette'; -import { Touchable } from './Touchable'; -import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu'; interface GestureOverlayProps { isActive: boolean; @@ -79,7 +76,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { private _overlayRef = React.createRef<HTMLDivElement>(); private _d1: Doc | undefined; private _inkToTextDoc: Doc | undefined; - private _thumbDoc: Doc | undefined; private thumbIdentifier?: number; private pointerIdentifier?: number; private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>(); @@ -93,78 +89,12 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { GestureOverlay.Instances.push(this); } - static setupThumbButtons(doc: Doc) { - const docProtoData: { title: string; icon: string; drag?: string; toolType?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [ - { title: 'use pen', icon: 'pen-nib', pointerUp: 'resetPen()', pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: 'blue' }, - { title: 'use highlighter', icon: 'highlighter', pointerUp: 'resetPen()', pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: 'yellow' }, - { - title: 'notepad', - icon: 'clipboard', - pointerUp: 'GestureOverlay.Instance.closeFloatingDoc()', - pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', - clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, isSystem: true }), - backgroundColor: 'orange', - }, - { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' }, - { title: 'ignore gestures', icon: 'signature', toolType: 'ignoregesture', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'green' }, - ]; - return docProtoData.map(data => - Docs.Create.FontIconDocument({ - _nativeWidth: 10, - _nativeHeight: 10, - _width: 10, - _height: 10, - title: data.title, - icon: data.icon, - toolType: data.toolType, - _dropAction: data.pointerDown ? 'copy' : undefined, - ignoreClick: data.ignoreClick, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - clipboard: data.clipboard, - onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, - onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, - backgroundColor: data.backgroundColor, - dragFactory: data.dragFactory, - isSystem: true, - }) - ); - } - - static setupThumbDoc(userDoc: Doc) { - if (!userDoc.thumbDoc) { - const thumbDoc = Docs.Create.LinearDocument(GestureOverlay.setupThumbButtons(userDoc), { - _width: 100, - _height: 50, - ignoreClick: true, - _lockedPosition: true, - title: 'buttons', - _layout_autoHeight: true, - _yMargin: 5, - linearViewIsExpanded: true, - backgroundColor: 'white', - isSystem: true, - }); - thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { - _width: 300, - _height: 25, - _layout_autoHeight: true, - linearViewIsExpanded: true, - flexDirection: 'column', - isSystem: true, - }); - userDoc.thumbDoc = thumbDoc; - } - return Cast(userDoc.thumbDoc, Doc); - } - componentWillUnmount() { GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1); GestureOverlay.Instance = GestureOverlay.Instances.lastElement(); } componentDidMount = () => { GestureOverlay.Instance = this; - this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc)); - this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc)); }; // TODO: nda - add dragging groups with one finger drag and have to click into group to scroll within the group @@ -395,24 +325,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { this.thumbIdentifier = thumb?.identifier; this._hands.set(thumb.identifier, fingers); - const others = fingers.filter(f => f !== thumb); - const minX = Math.min(...others.map(f => f.clientX)); - const minY = Math.min(...others.map(f => f.clientY)); - - // load up the palette collection around the thumb - const thumbDoc = await Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc); - if (thumbDoc) { - runInAction(() => { - RadialMenu.Instance._display = false; - this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc)); - this._thumbDoc = thumbDoc; - this._thumbX = thumb.clientX; - this._thumbY = thumb.clientY; - this._menuX = thumb.clientX + 50; - this._menuY = thumb.clientY; - this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; - }); - } this.removeMoveListeners(); document.removeEventListener('touchmove', this.handleHandMove); @@ -462,11 +374,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { if (Math.abs(pt.clientY - this._thumbY) > 10 * window.devicePixelRatio) { this._selectedIndex = Math.min(Math.max(-1, -Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1), this._possibilities.length - 1); } - } else if (this._thumbDoc) { - if (Math.abs(pt.clientX - this._thumbX) > 15 * window.devicePixelRatio) { - this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); - this._thumbX = pt.clientX; - } } } } @@ -485,7 +392,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier); this._palette = undefined; this.thumbIdentifier = undefined; - this._thumbDoc = undefined; // this chunk of code is for handling the ink to text toolglass let scriptWorked = false; @@ -1013,8 +919,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docRangeFilters={returnEmptyFilter} - docFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + childFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> ); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index b9b92dd2b..7b693c8da 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,22 +1,18 @@ import { random } from 'lodash'; import { action, runInAction } from 'mobx'; -import { DateField } from '../../fields/DateField'; import { Doc, DocListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; -import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, PromiseValue } from '../../fields/Types'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; -import { DocServer } from '../DocServer'; -import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { GroupManager } from '../util/GroupManager'; import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { CollectionFreeFormViewChrome } from './collections/CollectionMenu'; @@ -161,12 +157,10 @@ export class KeyManager { case 'delete': case 'backspace': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { - UndoManager.RunInBatch(() => { - if (LightboxView.LightboxDoc) { - LightboxView.SetLightboxDoc(undefined); - SelectionManager.DeselectAll(); - } else DocumentDecorations.Instance.onCloseClick(true); - }, 'backspace'); + if (LightboxView.LightboxDoc) { + LightboxView.SetLightboxDoc(undefined); + SelectionManager.DeselectAll(); + } else DocumentDecorations.Instance.onCloseClick(true); return { stopPropagation: true, preventDefault: true }; } break; @@ -267,6 +261,18 @@ export class KeyManager { case 't': PromiseValue(Cast(Doc.UserDoc()['tabs-button-tools'], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); break; + case 'i': + const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); + if (importBtn) { + MainView.Instance.selectMenu(importBtn); + } + break; + case 's': + const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); + if (trailsBtn) { + MainView.Instance.selectMenu(trailsBtn); + } + break; case 'f': if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) { SelectionManager.Views()[0].ComponentView?.search?.('', false, false); @@ -278,26 +284,26 @@ export class KeyManager { } break; case 'e': - Doc.ActiveTool = InkTool.Eraser; + Doc.ActiveTool = (Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser); break; case 'p': - Doc.ActiveTool = InkTool.Pen; - break; - case 'o': - const target = SelectionManager.Docs().lastElement(); - target && CollectionDockingView.OpenFullScreen(target); + Doc.ActiveTool = (Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen); break; case 'r': preventDefault = false; break; case 'y': - SelectionManager.DeselectAll(); - UndoManager.Redo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Redo(); + } stopPropagation = false; break; case 'z': - SelectionManager.DeselectAll(); - UndoManager.Undo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Undo(); + } stopPropagation = false; break; case 'a': @@ -352,38 +358,13 @@ export class KeyManager { }); public paste(e: ClipboardEvent) { - const plain = e.clipboardData?.getData('text/plain'); - const clone = plain?.startsWith('__DashCloneId('); - if (plain && (plain.startsWith('__DashDocId(') || clone)) { - const first = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - if (first?.props.Document.type === DocumentType.COL) { - const docids = plain.split(':'); - let count = 1; - const list: Doc[] = []; - const targetDataDoc = Doc.GetProto(first.props.Document); - const fieldKey = first.LayoutFieldKey; - const docList = DocListCast(targetDataDoc[fieldKey]); - docids.map( - (did, i) => - i && - DocServer.GetRefField(did).then(async doc => { - count++; - if (doc instanceof Doc) { - list.push(doc); - } - if (count === docids.length) { - const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d))); - if (added.length) { - added.map(doc => (doc.embedContainer = targetDataDoc)); - undoBatch(() => { - targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())); - })(); - } - } - }) - ); - } + const plain = e.clipboardData?.getData('text/plain'); // list of document ids, separated by ':'s + if (!plain) return; + const clone = plain.startsWith('__DashCloneId('); + const docids = plain.split(':'); // hack! docids[0] is the top left of the selection rectangle + const addDocument = SelectionManager.Views().lastElement()?.ComponentView?.addDocument; + if (addDocument && (plain.startsWith('__DashDocId(') || clone)) { + Doc.Paste(docids.slice(1), clone, addDocument); } } diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 9447b2e72..07e3270b1 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -182,8 +182,8 @@ export interface InkEndProps { inkDoc: Doc; inkView: InkingStroke; screenSpaceLineWidth: number; - startPt: PointData; - endPt: PointData; + startPt: () => PointData; + endPt: () => PointData; } @observer export class InkEndPtHandles extends React.Component<InkEndProps> { @@ -191,29 +191,31 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { @observable _overEnd: boolean = false; @action - dragRotate = (e: React.PointerEvent, p1: () => { X: number; Y: number }, p2: () => { X: number; Y: number }) => { + dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => { setupMoveUpEvents( this, e, action(e => { if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink'); // compute stretch factor by finding scaling along axis between start and end points - const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y }; - const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y }; + const p1 = pt1(); + const p2 = pt2(); + const v1 = { X: p1.X - p2.X, Y: p1.Y - p2.Y }; + const v2 = { X: e.clientX - p2.X, Y: e.clientY - p2.Y }; const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y); const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y); const scaling = v2len / v1len; const v1n = { X: v1.X / v1len, Y: v1.Y / v1len }; const v2n = { X: v2.X / v2len, Y: v2.Y / v2len }; const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y); - InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey); - InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2()); + InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2, v1n, e.shiftKey); + InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call return false; }), action(() => { this.props.inkView.controlUndo?.end(); this.props.inkView.controlUndo = undefined; - UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']); + UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']); }), returnFalse ); @@ -237,20 +239,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { ); return ( <svg> - {hdl('start', this.props.startPt, (e: React.PointerEvent) => - this.dragRotate( - e, - () => this.props.startPt, - () => this.props.endPt - ) - )} - {hdl('end', this.props.endPt, (e: React.PointerEvent) => - this.dragRotate( - e, - () => this.props.endPt, - () => this.props.startPt - ) - )} + {hdl('start', this.props.startPt(), (e: React.PointerEvent) => this.dragRotate(e, this.props.startPt, this.props.endPt))} + {hdl('end', this.props.endPt(), (e: React.PointerEvent) => this.dragRotate(e, this.props.endPt, this.props.startPt))} </svg> ); } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index d28981e17..abc4381a6 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -202,7 +202,6 @@ export class InkStrokeProperties { @action rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => { this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => { - view.rootDoc.rotation = NumCast(view.rootDoc.rotation) + angle; const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt); return !inkCenterPt ? ink diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index d6a6a64bf..6c213f40f 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -1,7 +1,8 @@ import * as iink from 'iink-js'; import { action, observable } from 'mobx'; import * as React from 'react'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { Cast, DateCast, NumCast } from '../../fields/Types'; import { aggregateBounds } from '../../Utils'; @@ -104,7 +105,7 @@ export class InkTranscription extends React.Component { : null; } - r.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); + r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); return (this._textRef = r); }; @@ -123,12 +124,12 @@ export class InkTranscription extends React.Component { const strokes: InkData[] = []; const times: number[] = []; validInks - .filter(i => Cast(i.data, InkField)) + .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField)) .forEach(i => { - const d = Cast(i.data, InkField, null); + const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null); const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke; strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); - times.push(DateCast(i.creationDate).getDate().getTime()); + times.push(DateCast(i.author_date).getDate().getTime()); }); this.currGroup = groupDoc; @@ -221,7 +222,7 @@ export class InkTranscription extends React.Component { const docList = DocListCast(this.currGroup.data); docList.forEach((inkDoc: Doc) => { // just having the times match up and be a unique value (actual timestamp doesn't matter) - const ms = DateCast(inkDoc.creationDate).getDate().getTime() + 14400000; + const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000; const word = timestampWord.get(ms); if (!word) { return; @@ -283,8 +284,8 @@ export class InkTranscription extends React.Component { action(d => { const x = NumCast(d.x); const y = NumCast(d.y); - const width = d[WidthSym](); - const height = d[HeightSym](); + const width = d[Width](); + const height = d[Height](); bounds.push({ x, y, width, height }); }) ); @@ -323,8 +324,8 @@ export class InkTranscription extends React.Component { // Gets a collection based on the selected nodes using a marquee view ref const newCollection = marqViewRef?.getCollection(selected, undefined, true); if (newCollection) { - newCollection.height = newCollection[HeightSym](); - newCollection.width = newCollection[WidthSym](); + newCollection.height = newCollection[Height](); + newCollection.width = newCollection[Width](); // if the grouping we are creating is an individual word if (word) { newCollection.title = word; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 14c5dc58d..d0210d63b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -21,9 +21,10 @@ Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class */ import React = require('react'); -import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { InkData, InkField } from '../../fields/InkField'; import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; @@ -87,7 +88,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { if (!addAsAnnotation && !pinProps) return this.rootDoc; - const anchor = Docs.Create.InkAnchorDocument({ title: 'Ink anchor:' + this.rootDoc.title, presDuration: 1100, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + const anchor = Docs.Create.ConfigDocument({ + title: 'Ink anchor:' + this.rootDoc.title, + // set presentation timing for restoring shape + presDuration: 1100, + presTransition: 1000, + annotationOn: this.rootDoc, + }); if (anchor) { anchor.backgroundColor = 'transparent'; // /* addAsAnnotation &&*/ this.addDocument(anchor); @@ -290,6 +297,18 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { */ nearestScreenPt = () => this._nearestScrPt; + @computed get screenCtrlPts() { + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + return inkData + .map(point => + this.screenToLocal() + .inverse() + .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2) + ) + .map(p => ({ X: p[0], Y: p[1] })); + } + startPt = () => this.screenCtrlPts[0]; + endPt = () => this.screenCtrlPts.lastElement(); /** * @param boundsLeft the screen space left coordinate of the ink stroke * @param boundsTop the screen space top coordinate of the ink stroke @@ -297,39 +316,31 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { */ componentUI = (boundsLeft: number, boundsTop: number) => { const inkDoc = this.props.Document; - const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + const { inkData, inkStrokeWidth } = this.inkScaledData(); const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.screenToLocal().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke const screenInkWidth = this.screenToLocal().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth); - const screenPts = inkData - .map(point => - this.screenToLocal() - .inverse() - .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2) - ) - .map(p => ({ X: p[0], Y: p[1] })); - const screenHdlPts = screenPts; const startMarker = StrCast(this.layoutDoc.stroke_startMarker); const endMarker = StrCast(this.layoutDoc.stroke_endMarker); - const markerScale = NumCast(this.layoutDoc.strokeMarkerScale); + const markerScale = NumCast(this.layoutDoc.stroke_markerScale); return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? ( !this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : ( <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> - <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> + <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={this.startPt} endPt={this.endPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> </div> ) ) : ( <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> {InteractionUtils.CreatePolyline( - screenPts, + this.screenCtrlPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, - StrCast(inkDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), + StrCast(inkDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), StrCast(inkDoc.stroke_bezier), 'none', startMarker, @@ -343,8 +354,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { 1.0, false )} - <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={screenHdlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> - <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} /> + <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> + <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} /> </div> ); }; @@ -357,17 +368,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { const startMarker = StrCast(this.layoutDoc.stroke_startMarker); const endMarker = StrCast(this.layoutDoc.stroke_endMarker); - const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1); + const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1); const closed = InkingStroke.IsClosed(inkData); const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask); const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent'; const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. - if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) { + if (isInkMask && (this.layoutDoc[Width]() !== Math.round(this.layoutDoc[Width]()) || this.layoutDoc[Height]() !== Math.round(this.layoutDoc[Height]()))) { setTimeout(() => { - this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[WidthSym]())); - this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[HeightSym]())); + this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[Width]())); + this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[Height]())); }); } @@ -379,8 +390,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { strokeColor, inkStrokeWidth, inkStrokeWidth, - StrCast(this.layoutDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), StrCast(this.layoutDoc.stroke_bezier), !closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor, startMarker, @@ -396,20 +407,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { ); const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); const highlightIndex = highlight?.highlightIndex; - const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); + 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, suppressFill: boolean = false, mask: boolean = false) => + const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) => InteractionUtils.CreatePolyline( inkData, inkLeft, inkTop, - highlightColor, + mask && highlightColor === 'transparent' ? strokeColor : highlightColor, inkStrokeWidth, inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), - StrCast(this.layoutDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), StrCast(this.layoutDoc.stroke_bezier), - !closed ? 'none' : !isInkMask && (fillColor === 'transparent' || suppressFill) ? 'none' : fillColor, + !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor, '', '', markerScale, @@ -423,7 +437,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { downHdlr, mask ); - const fsize = +StrCast(this.props.Document.fontSize, '12px').replace('px', ''); + 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. // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429 // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/ @@ -449,7 +463,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { cursor: this.props.isSelected() ? 'default' : undefined, }} {...interactions}> - {clickableLine(this.onPointerDown, undefined, isInkMask)} + {clickableLine(this.onPointerDown, isInkMask)} {isInkMask ? null : inkLine} </svg> {!closed || (!RTFCast(this.rootDoc.text)?.Text && (!this.props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : ( @@ -458,7 +472,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { style={{ color: StrCast(this.layoutDoc.textColor, 'black'), pointerEvents: this.props.isDocumentActive?.() ? 'all' : undefined, - width: this.layoutDoc[WidthSym](), + width: this.layoutDoc[Width](), transform: `scale(${this.props.NativeDimScaling?.() || 1})`, transformOrigin: 'top left', //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2, diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 1b309a6be..f79a30ad3 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -43,7 +43,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { @observable private static _layoutTemplateString: Opt<string>; @observable private static _doc: Opt<Doc>; @observable private static _docTarget: Opt<Doc>; - @observable private static _docFilters: string[] = []; // filters + @observable private static _childFilters: string[] = []; // filters private static _savedState: Opt<LightboxSavedState>; private static _history: Opt<{ doc: Doc; target?: Doc }[]> = []; @observable private static _future: Opt<Doc[]> = []; @@ -58,7 +58,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey; } if (!doc) { - this._docFilters && (this._docFilters.length = 0); + this._childFilters && (this._childFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; MainView.Instance._exploreMode = false; @@ -134,7 +134,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { @action public static SetCookie(cookie: string) { if (this.LightboxDoc && cookie) { - this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); + this._childFilters = (f => (this._childFilters ? [this._childFilters.push(f) as any, this._childFilters][1] : [f]))(`cookies:${cookie}:provide`); } } public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => { @@ -148,7 +148,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { layoutTemplate ); }; - docFilters = () => LightboxView._docFilters || []; + childFilters = () => LightboxView._childFilters || []; addDocTab = LightboxView.AddDocTab; @action public static Next() { const doc = LightboxView._doc!; @@ -261,8 +261,8 @@ export class LightboxView extends React.Component<LightboxViewProps> { renderDepth={0} rootSelected={returnTrue} docViewPath={returnEmptyDoclist} - docFilters={this.docFilters} - docRangeFilters={returnEmptyFilter} + childFilters={this.childFilters} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={undefined} removeDocument={undefined} @@ -313,8 +313,10 @@ export class LightboxView extends React.Component<LightboxViewProps> { className="lightboxView-tabBtn" title="open in tab" onClick={e => { + const lightdoc = LightboxView._docTarget || LightboxView._doc!; e.stopPropagation(); - CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none); + Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightdoc); + CollectionDockingView.AddSplit(lightdoc, OpenWhereMod.none); SelectionManager.DeselectAll(); LightboxView.SetLightboxDoc(undefined); }}> diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index c7a7614ac..a403a10e3 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -11,6 +11,7 @@ body { height: 100%; overflow: hidden; font-family: $sans-serif; + font-size: $body-text; margin: 0; position: absolute; top: 0; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6b18caed0..6dd1d53ee 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -6,19 +6,18 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { AssignAllExtensions } from '../../extensions/General/Extensions'; import { FieldLoader } from '../../fields/FieldLoader'; -import { DocServer } from '../DocServer'; -import { Docs } from '../documents/Documents'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; -import { LinkManager } from '../util/LinkManager'; // this must come before importing Docs and CurrentUserUtils 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'; dotenv.config(); AssignAllExtensions(); -FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure why this is needed to get the code loaded properly... +FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; // bcz: not sure why this is needed to get the code loaded properly... (async () => { MainView.Live = window.location.search.includes('live'); @@ -26,8 +25,12 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure root.render(<FieldLoader />); window.location.search.includes('safe') && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); - if (info.email === 'guest') DocServer.Control.makeReadOnly(); - await CurrentUserUtils.loadUserDocument(info.id); + // if (info.email === 'guest') DocServer.Control.makeReadOnly(); + if (!info.userDocumentId) { + alert('Fatal Error: user not found in database'); + return; + } + await CurrentUserUtils.loadUserDocument(info); setTimeout(() => { document.getElementById('root')!.addEventListener( 'wheel', @@ -45,9 +48,9 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000); const expires = 'expires=' + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; - new LinkManager(); new TrackMovements(); new ReplayMovements(); + new PingManager(); root.render(<MainView />); }, 0); })(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index b95ce0e99..b3faff442 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -47,6 +47,10 @@ h1, align-items: center; justify-content: space-between; gap: 10px; + background: rgb(0, 0, 0); + border-radius: 8px; + padding-left: 5px; + padding-right: 5px; } .mainView-snapLines { @@ -68,10 +72,6 @@ h1, left: 0; z-index: 1; touch-action: none; - - .searchBox-container { - background: $light-gray; - } } .mainView-container, @@ -118,10 +118,6 @@ h1, background: $light-gray; } - .searchBox-container { - background: $dark-gray; - } - .contextMenu-cont, .contextMenu-item { background: $dark-gray; @@ -198,10 +194,10 @@ h1, left: 0; position: absolute; z-index: 2; - background-color: $light-gray; + background-color: linen; //$light-gray; .editable-title { - background-color: $light-gray; + background-color: linen; //$light-gray; } } } @@ -250,7 +246,6 @@ h1, .mainView-leftMenuPanel { min-width: var(--menuPanelWidth); - background-color: $dark-gray; border-right: $standard-border; .collectionStackingView { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ce29b63cd..dc236b603 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -4,13 +4,13 @@ import * as far from '@fortawesome/free-regular-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'browndash-components/dist/styles/global.min.css'; -import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; +import { action, computed, configure, observable, runInAction } from 'mobx'; 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 { StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -21,7 +21,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; -import { ReportManager } from '../util/ReportManager'; +import { ReportManager } from '../util/reportManager/ReportManager'; import { RTFMarkup } from '../util/RTFMarkup'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SelectionManager } from '../util/SelectionManager'; @@ -81,7 +81,7 @@ export class MainView extends React.Component { @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel; @observable private _leftMenuFlyoutWidth: number = 0; @computed get _hideUI() { - return this.mainDoc && this.mainDoc._viewType !== CollectionViewType.Docking; + return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking; } @computed private get dashboardTabHeight() { @@ -153,46 +153,35 @@ export class MainView extends React.Component { } this._sidebarContent.proto = undefined; if (!MainView.Live) { - DocServer.setPlaygroundFields([ + DocServer.setLivePlaygroundFields([ 'dataTransition', 'viewTransition', 'treeViewOpen', - 'layout_showSidebar', + 'treeViewExpandedView', + 'carousel_index', 'itemIndex', // for changing slides in presentations 'layout_sidebarWidthPercent', 'layout_currentTimecode', 'layout_timelineHeightPercent', + 'layout_hideMinimap', + 'layout_showSidebar', + 'layout_scrollTop', + 'layout_fitWidth', + 'layout_curPage', 'presStatus', 'freeform_panX', 'freeform_panY', + 'freeform_scale', 'overlayX', 'overlayY', - 'layout_fitWidth', - 'nativeWidth', - 'nativeHeight', 'text_scrollHeight', 'text_height', - 'layout_hideMinimap', - 'freeform_scale', - 'layout_scrollTop', 'hidden', - 'layout_curPage', - 'viewType', + //'type_collection', 'chromeHidden', 'currentFrame', - 'width', - 'height', - 'nativeWidth', ]); // can play with these fields on someone else's } - DocServer.GetRefField('rtfProto').then( - proto => - proto instanceof Doc && - reaction( - () => StrCast(proto.BROADCAST_MESSAGE), - msg => msg && alert(msg) - ) - ); const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; @@ -217,7 +206,7 @@ export class MainView extends React.Component { window.removeEventListener('keydown', KeyManager.Instance.handle); window.removeEventListener('pointerdown', this.globalPointerDown, true); window.removeEventListener('pointermove', this.globalPointerMove, true); - window.removeEventListener('mouseclick', this.globalPointerClick, true); + window.removeEventListener('pointerup', this.globalPointerClick, true); window.removeEventListener('paste', KeyManager.Instance.paste as any); document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener); } @@ -236,7 +225,7 @@ export class MainView extends React.Component { if (pathname.length > 1 && pathname[0] === 'doc') { DocServer.GetRefField(pathname[1]).then( action(field => { - if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) { + if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) { Doc.GuestTarget = field; } }) @@ -487,6 +476,8 @@ export class MainView extends React.Component { fa.faSquareRootAlt, fa.faVolumeMute, fa.faUserCircle, + fa.faHeart, + fa.faHeartBroken, fa.faHighlighter, fa.faRemoveFormat, fa.faHandPointUp, @@ -528,12 +519,13 @@ export class MainView extends React.Component { }); initEventListeners = () => { + 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('mouseclick', this.globalPointerClick, true); + document.addEventListener('pointerup', this.globalPointerClick, true); document.addEventListener( 'click', (e: MouseEvent) => { @@ -569,7 +561,7 @@ export class MainView extends React.Component { @action createNewFolder = async () => { - const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }); Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); }; @@ -580,6 +572,8 @@ export class MainView extends React.Component { waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined); headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); + addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); + removeHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); @computed get headerBarDocView() { return ( <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}> @@ -587,18 +581,19 @@ export class MainView extends React.Component { key="headerBarDoc" Document={this.headerBarDoc} DataDoc={undefined} - addDocument={undefined} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={DefaultStyleProvider} rootSelected={returnTrue} - removeDocument={returnFalse} + addDocument={this.addHeaderDoc} + removeDocument={this.removeHeaderDoc} 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={this.headerBarScreenXf} childHideResizeHandles={returnTrue} + childDragAction="move" dontRegisterView={true} hideResizeHandles={true} PanelWidth={this.headerBarDocWidth} @@ -607,8 +602,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -636,8 +631,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} suppressSetHeight={true} renderDepth={this._hideUI ? 0 : -1} @@ -693,15 +688,13 @@ export class MainView extends React.Component { sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0); static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => { - const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereFields = location.split(':'); const keyValue = whereFields[1]?.includes('KeyValue'); const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none; - if (doc.dockingConfig) return DashboardView.openDashboard(doc); + if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc); // prettier-ignore switch (whereFields[0]) { case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location); - case OpenWhere.dashboard: return DashboardView.openDashboard(doc); - case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods); case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue); @@ -735,8 +728,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -747,7 +740,7 @@ export class MainView extends React.Component { @computed get leftMenuPanel() { return ( - <div key="menu" className="mainView-leftMenuPanel" style={{ display: LightboxView.LightboxDoc ? 'none' : undefined }}> + <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}> <DocumentView Document={Doc.MyLeftSidebarMenu} DataDoc={undefined} @@ -766,8 +759,8 @@ export class MainView extends React.Component { isContentActive={returnTrue} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} scriptContext={this} /> @@ -802,14 +795,17 @@ export class MainView extends React.Component { {this._hideUI ? null : this.leftMenuPanel} <div key="inner" className={`mainView-innerContent${this.colorScheme}`}> {this.flyout} - <div className="mainView-libraryHandle" style={{ left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}> - <FontAwesomeIcon icon="chevron-left" color={this.colorScheme === ColorScheme.Dark ? 'white' : 'black'} style={{ opacity: '50%' }} size="sm" /> + <div + className="mainView-libraryHandle" + style={{ background: StrCast(Doc.UserDoc().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" /> </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 }}> + <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> )} @@ -853,7 +849,7 @@ export class MainView extends React.Component { this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); - this._sidebarContent.proto = button.target as any; + this._sidebarContent.proto = DocCast(button.target); this.LastButton = button; }); @@ -864,7 +860,7 @@ export class MainView extends React.Component { this._leftMenuFlyoutWidth = 0; }); - remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); + remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true); @@ -876,7 +872,7 @@ export class MainView extends React.Component { @computed get docButtons() { return !Doc.MyDockedBtns ? null : ( - <div className="mainView-docButtons" ref={this._docBtnRef}> + <div className="mainView-docButtons" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }} ref={this._docBtnRef}> <CollectionLinearView Document={Doc.MyDockedBtns} DataDoc={undefined} @@ -902,8 +898,8 @@ export class MainView extends React.Component { renderDepth={0} focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>} @@ -914,11 +910,11 @@ export class MainView extends React.Component { return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : ( <div className="mainView-snapLines"> <svg style={{ width: '100%', height: '100%' }}> - {SnappingManager.horizSnapLines().map(l => ( - <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + {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'} /> ))} - {SnappingManager.vertSnapLines().map(l => ( - <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + {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'} /> ))} </svg> </div> @@ -957,7 +953,11 @@ export class MainView extends React.Component { render() { return ( <div - className={`mainView-container${this.colorScheme}`} + className={`mainView-container ${this.colorScheme}`} + style={{ + color: StrCast(Doc.UserDoc().userColor), + background: StrCast(Doc.UserDoc().userBackgroundColor), + }} onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)} ref={r => { r && @@ -1012,6 +1012,7 @@ export class MainView extends React.Component { <InkTranscription /> {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> + {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */} </div> ); } diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss index 0648e31c5..4bf9eb79f 100644 --- a/src/client/views/MainViewModal.scss +++ b/src/client/views/MainViewModal.scss @@ -4,22 +4,23 @@ z-index: 10000; width: 100%; height: 100%; + box-shadow: #00000044 5px 5px 10px; .dialogue-box { - padding: 10px; position: absolute; z-index: 1000; text-align: center; justify-content: center; align-self: center; align-content: center; - background: white; // border-radius: 10px; box-shadow: #00000044 5px 5px 10px; transform: translate(-50%, -50%); top: 50%; left: 50%; transition: 0.5s all ease; + border-radius: 10px; + overflow: hidden; } .overlay { @@ -28,6 +29,7 @@ position: absolute; z-index: 999; transition: 0.5s all ease; + backdrop-filter: blur(5px); } }
\ No newline at end of file diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 32997a944..42df99864 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import './MainViewModal.scss'; import { observer } from 'mobx-react'; +import { Doc } from '../../fields/Doc'; +import { StrCast } from '../../fields/Types'; +import { isDark } from 'browndash-components'; +import { Colors } from './global/globalEnums'; export interface MainViewOverlayProps { isDisplayed: boolean; @@ -41,9 +45,8 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> { className="overlay" onClick={this.props?.closeOnExternalClick} style={{ - backgroundColor: 'black', + backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? "#DFDFDF30" : "#32323230", ...(p.overlayStyle || {}), - opacity: p.isDisplayed ? overlayOpacity : 0, }} /> </div> diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index eb0fd0c7a..a4a2c1df9 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,6 +1,7 @@ import { action, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, Opt } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { NumCast } from '../../fields/Types'; @@ -125,7 +126,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { if (!e.aborted && e.linkDocument) { Doc.GetProto(e.linkDocument).link_relationship = 'cropped image'; Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title; - Doc.GetProto(e.linkDocument).layout_linkDisplay = false; + Doc.GetProto(e.linkDocument).link_displayLine = false; } }, }); @@ -153,8 +154,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { annotationOn: this.props.rootDoc, title: 'Annotation on ' + this.props.rootDoc.title, }); - marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); - marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop); + marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); + marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop); marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); anno.remove(); @@ -162,7 +163,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { return marqueeAnno; } - const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { + const textRegionAnno = Docs.Create.HTMLMarkerDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', @@ -207,7 +208,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { @action highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => { // creates annotation documents for current highlights - const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); + const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DocData]); const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc); return (annotationDoc as Doc) ?? undefined; @@ -243,7 +244,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this._top = Math.min(this._startY, this._startY + this._height); this._width = Math.abs(this._width); this._height = Math.abs(this._height); - e.stopPropagation(); + //e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged. if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor. }; onSelectEnd = (e: PointerEvent) => { diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 82ec5a5b3..bcbdd3ccb 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import "./MetadataEntryMenu.scss"; +import './MetadataEntryMenu.scss'; import { observer } from 'mobx-react'; import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; @@ -16,9 +16,9 @@ export interface MetadataEntryProps { } @observer -export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ - @observable private _currentKey: string = ""; - @observable private _currentValue: string = ""; +export class MetadataEntryMenu extends React.Component<MetadataEntryProps> { + @observable private _currentKey: string = ''; + @observable private _currentValue: string = ''; private _addChildren: boolean = false; @observable _allSuggestions: string[] = []; _suggestionDispser: IReactionDisposer | undefined; @@ -32,7 +32,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ if (!this.userModified) { this.previewValue(); } - } + }; previewValue = async () => { let field: Field | undefined | null = null; @@ -45,30 +45,30 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ if (field === null) { field = v; } else if (v !== field) { - value = "multiple values"; + value = 'multiple values'; } } if (value === undefined) { if (field !== null && field !== undefined) { - value = (onProto ? "" : "= ") + Field.toScriptString(field); + value = (onProto ? '' : '= ') + Field.toScriptString(field); } else { - value = ""; + value = ''; } } const s = value; - runInAction(() => this._currentValue = s); - } + runInAction(() => (this._currentValue = s)); + }; @action onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { this._currentValue = e.target.value; - this.userModified = e.target.value.trim() !== ""; - } + this.userModified = e.target.value.trim() !== ''; + }; @undoBatch @action onValueKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + if (e.key === 'Enter') { e.stopPropagation(); const script = KeyValueBox.CompileKVPScript(this._currentValue); if (!script) return; @@ -95,18 +95,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ this.clearInputs(); } } - } + }; @action clearInputs = () => { - this._currentKey = ""; - this._currentValue = ""; + this._currentKey = ''; + this._currentValue = ''; this.userModified = false; if (this.autosuggestRef.current) { const input: HTMLInputElement = (this.autosuggestRef.current as any).input; input && input.focus(); } - } + }; getKeySuggestions = (value: string) => { value = value.toLowerCase(); @@ -114,16 +114,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); - } + }; getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) => { - return (null); - } + return null; + }; componentDidMount() { - this._suggestionDispser = reaction(() => this._currentKey, - () => this._allSuggestions = this.getKeySuggestions(this._currentKey), - { fireImmediately: true }); + this._suggestionDispser = reaction( + () => this._currentKey, + () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)), + { fireImmediately: true } + ); } componentWillUnmount() { this._suggestionDispser && this._suggestionDispser(); @@ -131,49 +133,64 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ onClick = (e: React.ChangeEvent<HTMLInputElement>) => { this._addChildren = !this._addChildren; - } + }; private get considerChildOptions() { - if (!this.props.docs.every(doc => doc._viewType !== undefined)) { + if (!this.props.docs.every(doc => doc._type_collection !== undefined)) { return null; } return ( - <div style={{ display: "flex" }}> + <div style={{ display: 'flex' }}> Children: - <input type="checkbox" onChange={this.onClick} ></input> + <input type="checkbox" onChange={this.onClick}></input> </div> ); } _ref = React.createRef<HTMLInputElement>(); render() { - return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> - <div className="metadataEntry-inputArea"> - <div style={{ display: "flex", flexDirection: "row" }}> - <span>Key:</span> - <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={emptyPath} - alwaysRenderSuggestions={false} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={emptyFunction} - onSuggestionsClearRequested={emptyFunction} - ref={this.autosuggestRef} /> + return ( + <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> + <div className="metadataEntry-inputArea"> + <div style={{ display: 'flex', flexDirection: 'row' }}> + <span>Key:</span> + <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}> + <Autosuggest + inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} + getSuggestionValue={this.getSuggestionValue} + suggestions={emptyPath} + alwaysRenderSuggestions={false} + renderSuggestion={this.renderSuggestion} + onSuggestionsFetchRequested={emptyFunction} + onSuggestionsClearRequested={emptyFunction} + ref={this.autosuggestRef} + /> + </div> </div> + <div style={{ display: 'flex', flexDirection: 'row' }}> + <span>Value:</span> + <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + </div> + {this.considerChildOptions} </div> - <div style={{ display: "flex", flexDirection: "row" }}> - <span>Value:</span> - <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + <div className="metadataEntry-keys"> + <ul> + {this._allSuggestions + .slice() + .sort() + .map(s => ( + <li + key={s} + onClick={action(() => { + this._currentKey = s; + this.previewValue(); + })}> + {s} + </li> + ))} + </ul> </div> - {this.considerChildOptions} - </div> - <div className="metadataEntry-keys" > - <ul> - {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)} - </ul> </div> - </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 033cdf1f7..5362bf9f0 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -5,7 +5,6 @@ width: 100vw; height: 100vh; /* background-color: pink; */ - z-index: 100000; } .overlayWindow-outerDiv { @@ -55,8 +54,8 @@ } .overlayView-doc { - z-index: 9002; //so that it appears above chroma + z-index: 9002; //so that it appears above chroma position: absolute; top: 0; left: 0; -}
\ No newline at end of file +} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 9baedae6d..339507f65 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -3,7 +3,8 @@ import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; import ReactLoading from 'react-loading'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { NumCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils'; @@ -159,7 +160,7 @@ export class OverlayView extends React.Component { } removeOverlayDoc = (doc: Doc | Doc[]) => { - (doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc)); + (doc instanceof Doc ? [doc] : doc).forEach(Doc.RemFromMyOverlay); return true; }; @@ -177,6 +178,7 @@ export class OverlayView extends React.Component { offsety = 0; const dref = React.createRef<HTMLDivElement>(); const onPointerMove = action((e: PointerEvent, down: number[]) => { + if (e.cancelBubble) return false; // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped if (e.buttons === 1) { d.overlayX = e.clientX + offsetx; d.overlayY = e.clientY + offsety; @@ -196,7 +198,7 @@ export class OverlayView extends React.Component { }); const onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); + setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction, false); offsetx = NumCast(d.overlayX) - e.clientX; offsety = NumCast(d.overlayY) - e.clientY; }; @@ -213,8 +215,8 @@ export class OverlayView extends React.Component { bringToFront={emptyFunction} addDocument={undefined} removeDocument={this.removeOverlayDoc} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} + PanelWidth={d[Width]} + PanelHeight={d[Height]} ScreenToLocalTransform={this.docScreenToLocalXf(d)} renderDepth={1} hideDecorations={true} @@ -226,8 +228,8 @@ export class OverlayView extends React.Component { docViewPath={returnEmptyDoclist} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss deleted file mode 100644 index 0ec879288..000000000 --- a/src/client/views/Palette.scss +++ /dev/null @@ -1,30 +0,0 @@ -.palette-container { - .palette-thumb { - touch-action: pan-x; - position: absolute; - height: 70px; - overflow: hidden; - - .palette-thumbContent { - transition: transform .3s; - width: max-content; - overflow: hidden; - - .collectionView { - overflow: visible; - - .collectionLinearView-outer { - overflow: visible; - } - } - } - - .palette-cover { - width: 50px; - height: 50px; - position: absolute; - bottom: 0; - border: 1px solid black; - } - } -}
\ No newline at end of file diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx deleted file mode 100644 index 3ad28c418..000000000 --- a/src/client/views/Palette.tsx +++ /dev/null @@ -1,69 +0,0 @@ -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} - docFilters={returnEmptyFilter} - docRangeFilters={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.tsx b/src/client/views/PreviewCursor.tsx index f7fb443c0..82d2bff56 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -2,9 +2,8 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; -import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { ImageUtils } from '../util/Import & Export/ImageUtils'; import { Transform } from '../util/Transform'; @@ -51,38 +50,28 @@ export class PreviewCursor extends React.Component<{}> { PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, PreviewCursor._addDocument).then(batch.end); } else if (re.test(plain)) { const url = plain; - undoBatch(() => - PreviewCursor._addDocument( - Docs.Create.WebDocument(url, { - title: url, - _width: 500, - _height: 300, - data_useCors: true, - x: newPoint[0], - y: newPoint[1], - }) - ) - )(); + if (url.startsWith(window.location.href)) { + undoBatch(() => + PreviewCursor._addDocument( + Docs.Create.WebDocument(url, { + title: url, + _width: 500, + _height: 300, + data_useCors: true, + x: newPoint[0], + y: newPoint[1], + }) + ) + )(); + } else alert('cannot paste dash into itself'); } else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) { const clone = plain.startsWith('__DashCloneId('); const docids = plain.split(':'); - const strs = docids[0].split(','); + const strs = docids[0].split(','); // hack! docids[0] is the top left of the selection rectangle const ptx = Number(strs[0].substring((clone ? '__DashCloneId(' : '__DashDocId(').length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); + Doc.Paste(docids.slice(1), clone, PreviewCursor._addDocument, ptx, pty, newPoint); - const batch = UndoManager.StartBatch('cloning'); - { - const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null))); - const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy; - const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0; - const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0; - docs.map(doc => { - doc.x = NumCast(doc.x) - firstx; - doc.y = NumCast(doc.y) - firsty; - }); - PreviewCursor._addDocument(docs); - } - batch.end(); e.stopPropagation(); } else { FormattedTextBox.PasteOnLoad = e; diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index 36b2df73e..b801b3abf 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -20,22 +20,31 @@ $linkGap : 3px; .propertiesButtons-linkButton-empty, .propertiesButtons-linkButton-nonempty { - height: 25px; - width: 29px; - border-radius: 6px; - pointer-events: auto; - background-color: $dark-gray; - color: #fcfbf7; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - transition: transform 0.2s; - text-align: center; + // margin-right: 7px; + // margin-left: 8px; + height: 28px; + // width: 226px;//29px; display: flex; - justify-content: center; align-items: center; - margin-right: 10px; - margin-left: 4px; + // height: 25px; + // width: 230px;//29px; + // display: flex; + // align-items: center; + // border-radius: 6px; + pointer-events: auto; + // background-color: $dark-gray; + // color: #fcfbf7; + // text-transform: uppercase; + // letter-spacing: 2px; + // font-size: 75%; + transition: transform 0.2s; + // text-align: center; + + + // justify-content: center; + + // margin-right: 10px; + // margin-left: 4px; &:hover { background: $medium-gray; @@ -46,25 +55,34 @@ $linkGap : 3px; .propertiesButtons-linkButton-empty.toggle-on { background-color: $medium-blue; color: $white; + width:100% } .propertiesButtons-linkButton-empty.toggle-hover { background-color: $light-blue; color: $black; + width:100% } .propertiesButtons-linkButton-empty.toggle-off { - background-color: $dark-gray; - color: white; + background-color: white;//$dark-gray; + color: black; //white; + width:100% +} + +.propertiesButtons-icon { + margin-left:8px; } .propertiesButtons { - margin-top: 3px; - grid-column: 1/4; + position:relative; width: 100%; - height: auto; - display: flex; - flex-direction: row; - flex-wrap: wrap; - padding-bottom: 5.5px; + // margin-top: 3px; +// // grid-column: 1/4; +// width: 100%; +// height: auto; +// display: flex; +// // flex-direction: row; +// // flex-wrap: wrap; +// padding-bottom: 5.5px; } .onClickFlyout-editScript { @@ -80,8 +98,8 @@ $linkGap : 3px; .propertiesButtons-button { pointer-events: auto; - padding-right: 5px; - width: 25px; + padding-right: 8px;//5px; + width: 100%;//width: 25px; border-radius: 5px; margin-right: 20px; margin-bottom: 8px; @@ -110,18 +128,24 @@ $linkGap : 3px; } } -.propertiesButtons-title { - background: $dark-gray; - color: $white; - font-size: 6px; - width: 37px; - padding: 3px; - height: 12px; - border-radius: 7px; +.propertiesButtons-label { text-transform: uppercase; - text-align: center; - margin-top: -4px; -} + margin-left: 8px; + // margin-right: 50 px; +} + +// .propertiesButtons-title { +// background: pink; //$dark-gray; +// color: $white; +// font-size: 6px; +// width: 37px; +// padding: 3px; +// height: 12px; +// border-radius: 7px; +// text-transform: uppercase; +// text-align: center; +// margin-top: -4px; +// } .propertiesButtons-linker { height: 25px; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 86230ca78..8cae34d7d 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,27 +1,37 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { Icon, Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Utils } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { IsFollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; -import { undoBatch } from '../util/UndoManager'; +import { undoable, undoBatch } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { pasteImageBitmap } from './nodes/WebBoxRenderer'; import './PropertiesButtons.scss'; import React = require('react'); -import { IsFollowLinkScript } from '../util/LinkFollower'; +import { JsxElement } from 'typescript'; +import { FaBraille, FaHighlighter, FaLock, FaLockOpen, FaThumbtack } from 'react-icons/fa'; +import { AiOutlineApple, AiOutlineColumnWidth, AiOutlinePicture } from 'react-icons/ai'; +import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtitles, MdSubtitlesOff, MdTouchApp } from 'react-icons/md'; +import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; +import { BiHide, BiShow } from 'react-icons/bi'; +import { BsGrid3X3GapFill } from 'react-icons/bs'; +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; @@ -31,6 +41,7 @@ enum UtilityButtonState { OpenRight, OpenExternally, } + @observer export class PropertiesButtons extends React.Component<{}, {}> { @observable public static Instance: PropertiesButtons; @@ -42,251 +53,354 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !SelectionManager.SelectedSchemaDoc() && SelectionManager.Views().lastElement()?.topMost; } - propertyToggleBtn = (label: string, property: string, tooltip: (on?: any) => string, icon: (on: boolean) => string, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { + propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedDoc; const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? false : true); return !targetDoc ? null : ( - <Tooltip title={<div className={`dash-tooltip`}>{tooltip(targetDoc?.[property])} </div>} placement="top"> - <div> - <div - className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`} - onPointerDown={e => e.stopPropagation()} - onClick={undoBatch(() => { - if (SelectionManager.Views().length > 1) { - SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); - } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); - })}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} /> - </div> - <div className="propertiesButtons-title">{label}</div> - </div> - </Tooltip> + <Toggle + toggleStatus={BoolCast(targetDoc[property])} + text={label(targetDoc?.[property])} + color={StrCast(Doc.UserDoc().userColor)} + icon={icon(targetDoc?.[property] as any)} + iconPlacement={'left'} + align={'flex-start'} + fillWidth={true} + toggleType={ToggleType.BUTTON} + onClick={undoable(() => { + if (SelectionManager.Views().length > 1) { + SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); + } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); + }, property)} + /> ); }; + + // this implments a container pattern by marking the targetDoc (collection) as a lightbox + // that always fits its contents to its container and that hides all other documents when + // a link is followed that targets a 'lightbox' destination + @computed get isLightboxButton() { + return this.propertyToggleBtn( + on => 'Lightbox', + 'isLightbox', + on => `${on ? 'Set' : 'Remove'} lightbox flag`, + on => 'window-restore', + onClick => { + SelectionManager.Views().forEach(dv => { + const containerDoc = dv.rootDoc; + //containerDoc.followAllLinks = + // containerDoc.noShadow = + // containerDoc.layout_disableBrushing = + // containerDoc._forceActive = + //containerDoc._freeform_fitContentsToBox = + containerDoc._isLightbox = !containerDoc._isLightbox; + //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; + const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]); + //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' }); + containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false))); + }); + } + ); + } + + @computed get titleButton() { + return this.propertyToggleBtn( + on => (!on ? 'SHOW TITLE' : this.selectedDoc?.['_layout_showTitle'] === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'), + '_layout_showTitle', + on => 'Switch between title styles', + on => (on ? <MdSubtitlesOff /> : <MdSubtitles />), // {currentIcon}, //(on ? <MdSubtitles/> :) , //,'text-width', on ? <MdSubtitles/> : <MdSubtitlesOff/>, + (dv, doc) => { + const tdoc = dv?.rootDoc || doc; + const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : ''; + tdoc._layout_showTitle = newtitle ? newtitle : undefined; + } + ); + } + @computed get lockButton() { return this.propertyToggleBtn( - 'No\xA0Drag', + on => (on ? 'UNLOCK' : 'LOCK'), // 'No\xA0Drag', '_lockedPosition', on => `${on ? 'Unlock' : 'Lock'} position to prevent dragging`, - on => 'thumbtack' + on => (on ? <FaLockOpen /> : <FaLock />) + // on => 'thumbtack' ); } + @computed get maskButton() { + //highlight text while going down and reading through return this.propertyToggleBtn( - 'Mask', + on => (on ? 'PLAIN INK' : 'HIGHLIGHTER MASK'), 'stroke_isInkMask', on => (on ? 'Make plain ink' : 'Make highlight mask'), - on => 'paint-brush', + on => (on ? <TbHighlightOff /> : <TbHighlight />), // <FaHighlighter/>,// 'paint-brush', (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc) ); } + @computed get hideImageButton() { + // put in developer -- can trace on top of object and drawing is still there return this.propertyToggleBtn( - 'Background', + on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), //'Background', '_hideImage', on => (on ? 'Show Image' : 'Show Background'), - on => 'portrait' + on => (on ? <BiShow /> : <BiHide />) //'portrait' ); } + @computed get clustersButton() { return this.propertyToggleBtn( - 'Clusters', + on => (on ? 'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'), '_freeform_useClusters', on => `${on ? 'Hide' : 'Show'} clusters`, - on => 'braille' + on => <FaBraille /> ); } @computed get panButton() { return this.propertyToggleBtn( - 'Lock\xA0View', + on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), //'Lock\xA0View', '_lockedTransform', on => `${on ? 'Unlock' : 'Lock'} panning of view`, - on => 'lock' + on => (on ? <TbHandStop /> : <TbHandOff />) //'lock' ); } + @computed get forceActiveButton() { + //select text return this.propertyToggleBtn( - 'Active', + on => (on ? 'INACTIVE INTERACTION' : 'ACTIVE INTERACTION'), '_forceActive', on => `${on ? 'Select to activate' : 'Contents always active'} `, - on => 'eye' + on => <MdTouchApp /> // 'eye' ); } + @computed get fitContentButton() { return this.propertyToggleBtn( - 'View All', + on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All', '_freeform_fitContentsToBox', on => `${on ? "Don't" : 'Do'} fit content to container visible area`, - on => 'object-group' - ); - } - // this implments a container pattern by marking the targetDoc (collection) as a lightbox - // that always fits its contents to its container and that hides all other documents when - // a link is followed that targets a 'lightbox' destination - @computed get isLightboxButton() { - return this.propertyToggleBtn( - 'Lightbox', - 'isLightbox', - on => `${on ? 'Set' : 'Remove'} lightbox flag`, - on => 'window-restore', - onClick => { - SelectionManager.Views().forEach(dv => { - const containerDoc = dv.rootDoc; - //containerDoc.followAllLinks = - // containerDoc.noShadow = - // containerDoc.disableDocBrushing = - // containerDoc._forceActive = - //containerDoc._freeform_fitContentsToBox = - containerDoc._isLightbox = !containerDoc._isLightbox; - //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; - const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]); - //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' }); - containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false))); - }); - } + on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) //'object-group' ); } + + // // this implments a container pattern by marking the targetDoc (collection) as a lightbox + // // that always fits its contents to its container and that hides all other documents when + // // a link is followed that targets a 'lightbox' destination + // @computed get isLightboxButton() { // developer + // return this.propertyToggleBtn( + // on => 'Lightbox', + // 'isLightbox', + // on => `${on ? 'Set' : 'Remove'} lightbox flag`, + // on => 'window-restore', + // onClick => { + // SelectionManager.Views().forEach(dv => { + // const containerDoc = dv.rootDoc; + // //containerDoc.followAllLinks = + // // containerDoc.noShadow = + // // containerDoc.disableDocBrushing = + // // containerDoc._forceActive = + // //containerDoc._freeform_fitContentsToBox = + // containerDoc._isLightbox = !containerDoc._isLightbox; + // //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; + // const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]); + // //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' }); + // containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false))); + // }); + // } + // ); + // } + @computed get layout_fitWidthButton() { return this.propertyToggleBtn( - 'Fit\xA0Width', + on => (on ? 'RESTRICT WIDTH' : 'FIT WIDTH'), //'Fit\xA0Width', '_layout_fitWidth', on => `${on ? "Don't" : 'Do'} fit content to width of container`, - on => 'arrows-alt-h' + on => (on ? <AiOutlineColumnWidth /> : <RxWidth />) // 'arrows-alt-h' ); } + @computed get captionButton() { return this.propertyToggleBtn( - 'Caption', + //DEVELOPER + on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), //'Caption', '_layout_showCaption', on => `${on ? 'Hide' : 'Show'} caption footer`, - on => 'closed-captioning', + on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), //'closed-captioning', (dv, doc) => ((dv?.rootDoc || doc)._layout_showCaption = (dv?.rootDoc || doc)._layout_showCaption === undefined ? 'caption' : undefined) ); } + @computed get chromeButton() { + // developer -- removing UI decoration return this.propertyToggleBtn( - 'Controls', + on => (on ? 'ENABLE UI CONTROLS' : 'DISABLE UI CONTROLS'), '_chromeHidden', on => `${on ? 'Show' : 'Hide'} editing UI`, - on => 'edit', + on => (on ? <TbEditCircle /> : <TbEditCircleOff />), // 'edit', (dv, doc) => ((dv?.rootDoc || doc)._chromeHidden = !(dv?.rootDoc || doc)._chromeHidden) ); } - @computed get titleButton() { - return this.propertyToggleBtn( - 'Title', - '_layout_showTitle', - on => 'Switch between title styles', - on => 'text-width', - (dv, doc) => { - const tdoc = dv?.rootDoc || doc; - const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : ''; - tdoc._layout_showTitle = newtitle; - } - ); - } + @computed get layout_autoHeightButton() { + // store previous dimensions to store old values return this.propertyToggleBtn( - 'Auto\xA0Size', + on => (on ? 'AUTO\xA0SIZE' : 'FIXED SIZE'), '_layout_autoHeight', on => `Automatical vertical sizing to show all content`, - on => 'arrows-alt-v' + on => <FontAwesomeIcon icon="arrows-alt-v" size="lg" /> ); } + @computed get gridButton() { return this.propertyToggleBtn( - 'Grid', + on => (on ? 'HIDE GRID' : 'DISPLAY GRID'), '_freeform_backgroundGrid', on => `Display background grid in collection`, - on => 'border-all' - ); - } - @computed get groupButton() { - return this.propertyToggleBtn( - 'Group', - 'isGroup', - on => `Display collection as a Group`, - on => 'object-group', - (dv, doc) => { - doc.isGroup = !doc.isGroup; - doc.forceActive = doc.isGroup; - } - ); - } - @computed get freezeThumb() { - return this.propertyToggleBtn( - 'FreezeThumb', - '_thumb-frozen', - on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`, - on => 'snowflake', - (dv, doc) => { - if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined; - else { - document.body.focus(); // so that we can access the clipboard without an error - setTimeout(() => - pasteImageBitmap((data_url: any, error: any) => { - error && console.log(error); - data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename))); - }) - ); - } - } + on => (on ? <MdGridOff /> : <MdGridOn />) //'border-all' ); } + + // @computed get groupButton() { //developer + // return this.propertyToggleBtn( + // on => 'Group', + // 'isGroup', + // on => `Display collection as a Group`, + // on => 'object-group', + // (dv, doc) => { + // doc.isGroup = !doc.isGroup; + // doc.forceActive = doc.isGroup; + // } + // ); + // } + + // @computed get freezeThumb() { + // return this.propertyToggleBtn( + // 'FreezeThumb', + // '_thumb-frozen', + // on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`, + // on => 'snowflake', + // (dv, doc) => { + // if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined; + // else { + // document.body.focus(); // so that we can access the clipboard without an error + // setTimeout(() => + // pasteImageBitmap((data_url: any, error: any) => { + // error && console.log(error); + // data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename))); + // }) + // ); + // } + // } + // ); + // } @computed get snapButton() { + // THESE ARE NOT COMING return this.propertyToggleBtn( - 'Snap\xA0Lines', + on => (on ? 'HIDE SNAP LINES' : 'SHOW SNAP LINES'), 'freeform_snapLines', on => `Display snapping lines when objects are dragged`, - on => 'th', - undefined, - true + on => <TfiBarChart />, //'th', + undefined ); } - @computed - get onClickButton() { - return !this.selectedDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top"> - <div> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}> - <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> - </div> - </Flyout> - </div> - <div className="propertiesButtons-title"> onclick </div> - </div> - </Tooltip> - ); - } - @computed - get perspectiveButton() { - return !this.selectedDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">Choose view perspective</div>} placement="top"> - <div> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onPerspectiveFlyout}> - <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> - </div> - </Flyout> - </div> - <div className="propertiesButtons-title"> Perspective </div> - </div> - </Tooltip> - ); - } + // @computed + // get onClickButton() { + // return !this.selectedDoc ? null : ( + // <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top"> + // <div> + // <div className="propertiesButtons-linkFlyout"> + // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}> + // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}> + // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> + // </div> + // </Flyout> + // </div> + // <div className="propertiesButtons-title"> onclick </div> + // </div> + // </Tooltip> + // ); + // } + // @computed + // get perspectiveButton() { // gone + // return !this.selectedDoc ? null : ( + // <Tooltip title={<div className="dash-tooltip">Choose view perspective</div>} placement="top"> + // <div> + // <div className="propertiesButtons-linkFlyout"> + // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onPerspectiveFlyout}> + // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}> + // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> + // </div> + // </Flyout> + // </div> + // <div className="propertiesButtons-title"> Perspective </div> + // </div> + // </Tooltip> + // ); + // } @undoBatch handlePerspectiveChange = (e: any) => { - this.selectedDoc && (this.selectedDoc._viewType = e.target.value); + this.selectedDoc && (this.selectedDoc._type_collection = e.target.value); SelectionManager.Views() .filter(dv => dv.docView) .map(dv => dv.docView!) - .forEach(docView => (docView.layoutDoc._viewType = e.target.value)); + .forEach(docView => (docView.layoutDoc._type_collection = e.target.value)); }; + @computed get onClickVal() { + const linkButton = IsFollowLinkScript(this.selectedDoc.onClick); + const followLoc = this.selectedDoc._followLinkLocation; + const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); + + if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace'; + else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight'; + else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal'; + else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail'; + else return 'nothing'; + } + + @computed + get onClickButton() { + const buttonList = [ + ['nothing', 'Select Document'], + ['enterPortal', 'Enter Portal'], + ['toggleDetail', 'Toggle Detail'], + ['linkInPlace', 'Open Link in Lightbox'], + ['linkOnRight', 'Open Link on Right'], + ]; + + const items: IListItemProps[] = buttonList.map(value => { + return { + text: value[1], + val: value[1], + }; + }); + return !this.selectedDoc ? null : ( + <Dropdown + tooltip={'Choose onClick behavior'} + items={items} + selectedVal={this.onClickVal} + setSelectedVal={val => this.handleOptionChange(val as string)} + title={'Choose onClick behaviour'} + color={StrCast(Doc.UserDoc().userColor)} + dropdownType={DropdownType.SELECT} + type={Type.SEC} + fillWidth + /> + // <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top"> + // <div> + // <div className="propertiesButtons-linkFlyout"> + // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}> + // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}> + // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> + // </div> + // </Flyout> + // </div> + // <div className="propertiesButtons-title"> onclick </div> + // </div> + // </Tooltip> + ); + } @undoBatch @action @@ -366,26 +480,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { </div> ); } - @computed - get onPerspectiveFlyout() { - const excludedViewTypes = [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear]; - - const makeLabel = (value: string, label: string) => ( - <div className="radio" key={label}> - <label> - <input type="radio" value={value} checked={(this.selectedDoc?._viewType ?? 'invalid') === value} onChange={this.handlePerspectiveChange} /> - {label} - </label> - </div> - ); - return ( - <form> - {Object.values(CollectionViewType) - .filter(type => !excludedViewTypes.includes(type)) - .map(type => makeLabel(type, type))} - </form> - ); - } render() { const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)]; @@ -395,9 +489,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isMap = this.selectedDoc?.type === DocumentType.MAP; const isCollection = this.selectedDoc?.type === DocumentType.COL; //TODO: will likely need to create separate note-taking view type here - const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking; - const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform; - const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree; + const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking; + const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform; + const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree; const isTabView = this.selectedTabView; const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => ( <div className="propertiesButtons-button" style={style}> @@ -411,22 +505,21 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.titleButton)} {toggle(this.captionButton)} {toggle(this.lockButton)} - {toggle(this.onClickButton)} + {/* {toggle(this.onClickButton)} */} {toggle(this.layout_fitWidthButton)} - {toggle(this.freezeThumb)} - {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })} + {/* {toggle(this.freezeThumb)} */} + {toggle(this.forceActiveButton)} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })} - {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} + {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */} {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.gridButton, { display: !isCollection ? 'none' : '' })} - {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })} + {/* {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })} */} {toggle(this.snapButton, { display: !isCollection ? 'none' : '' })} {toggle(this.clustersButton, { display: !isFreeForm ? 'none' : '' })} {toggle(this.panButton, { display: !isFreeForm ? 'none' : '' })} - {toggle(this.perspectiveButton, { display: !isCollection || isNovice ? 'none' : '' })} </div> ); } diff --git a/src/client/views/PropertiesDocBacklinksSelector.scss b/src/client/views/PropertiesDocBacklinksSelector.scss index 4d2a61c3b..5c53acf48 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.scss +++ b/src/client/views/PropertiesDocBacklinksSelector.scss @@ -1,4 +1,4 @@ -.propertiesView-contexts-content { +.propertiesDocBacklinksSelector { .linkMenu { position: relative; } diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index 46e6fd188..da4438481 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -19,19 +19,19 @@ type PropertiesDocBacklinksSelectorProps = { @observer export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> { getOnClick = (link: Doc) => { - const linkSource = this.props.Document; - const other = LinkManager.getOppositeAnchor(link, linkSource); + 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; if (otherdoc) { otherdoc.hidden = false; - this.props.addDocTab(Doc.IsDocDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); + this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); } }; render() { return !SelectionManager.Views().length ? null : ( - <div className="preroptiesDocBacklinksSelector"> + <div className="propertiesDocBacklinksSelector"> {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/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 12a09196e..d157e7b1c 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -3,11 +3,10 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; -import { CollectionViewType } from '../documents/DocumentTypes'; +import { Cast, StrCast } from '../../fields/Types'; import { DocFocusOrOpen } from '../util/DocumentManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; +import { DocumentView, OpenWhere } from './nodes/DocumentView'; import './PropertiesDocContextSelector.scss'; type PropertiesDocContextSelectorProps = { @@ -39,6 +38,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC }, new Set<Doc>()) .keys() ); + return doclayouts .filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance?.props.Document)) .filter(doc => !Doc.IsSystem(doc)) @@ -48,11 +48,12 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC getOnClick = (col: Doc, target: Doc) => { if (!this.props.DocView) return; - col = Doc.IsDocDataProto(col) ? Doc.MakeDelegate(col) : col; + col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col; DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col); }; render() { + if (this._docs.length < 1) return undefined; return ( <div> {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>} diff --git a/src/client/views/PropertiesSection.scss b/src/client/views/PropertiesSection.scss new file mode 100644 index 000000000..321b6300c --- /dev/null +++ b/src/client/views/PropertiesSection.scss @@ -0,0 +1,24 @@ +@import './global/globalCssVariables.scss'; + +.propertiesView-section { + + .propertiesView-content { + padding: 10px; + } + + .propertiesView-sectionTitle { + text-align: center; + display: flex; + padding: 3px 10px; + font-size: 14px; + font-weight: bold; + justify-content: space-between; + align-items: center; + + .propertiesView-sectionTitle-icon { + width: 20px; + height: 20px; + align-items: flex-end; + } + } +} diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx new file mode 100644 index 000000000..6fab0168b --- /dev/null +++ b/src/client/views/PropertiesSection.tsx @@ -0,0 +1,64 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import './PropertiesSection.scss'; +import { Doc } from '../../fields/Doc'; +import { StrCast } from '../../fields/Types'; + +export interface PropertiesSectionProps { + title: string; + content?: JSX.Element | string | null; + isOpen: boolean; + setIsOpen: (bool: boolean) => any; + inSection?: boolean; + setInSection?: (bool: boolean) => any; + onDoubleClick?: () => void; +} + +@observer +export class PropertiesSection extends React.Component<PropertiesSectionProps> { + @computed get color() { + return StrCast(Doc.UserDoc().userColor); + } + + @computed get backgroundColor() { + return StrCast(Doc.UserDoc().userBackgroundColor); + } + + @computed get variantColor() { + return StrCast(Doc.UserDoc().userVariantColor); + } + + @observable isDouble: boolean = false; + + render() { + if (this.props.content === undefined || this.props.content === 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))}> + <div + className="propertiesView-sectionTitle" + onDoubleClick={action(e => { + this.isDouble = true; + this.props.onDoubleClick && this.props.onDoubleClick(); + this.props.setIsOpen(true); + setTimeout(() => (this.isDouble = false), 300); + })} + onClick={action(e => { + this.props.setIsOpen(!this.props.isOpen); + })} + style={{ + background: this.props.isOpen ? this.variantColor : this.backgroundColor, + color: this.color, + }}> + {this.props.title} + <div className="propertiesView-sectionTitle-icon"> + <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>} + </div> + ); + } +} diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 897be9a32..b79486167 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -6,122 +6,31 @@ font-family: 'Roboto'; font-size: 12px; cursor: auto; + border-left: $standard-border; .slider-text { font-size: 8px; } - overflow-x: hidden; - overflow-y: auto; - .propertiesView-title { - text-align: center; - padding-top: 12px; - padding-bottom: 12px; - display: flex; - font-size: 18px; + padding: 10px; + font-size: 24px; font-weight: bold; - justify-content: center; - - .propertiesView-title-icon { - width: 20px; - height: 20px; - padding-left: 38px; - margin-top: -5px; - align-items: flex-end; - margin-left: auto; - margin-right: 10px; - - &:hover { - color: grey; - cursor: pointer; - } - } } - .propertiesView-name { - border-bottom: 1px solid black; - padding: 8.5px; - font-size: 12.5px; + overflow-x: hidden; + overflow-y: auto; - &:hover { - cursor: text; - } + .propertiesView-name, + .propertiesView-type { + padding: 5px 10px; } - .propertiesView-settings { - //border-bottom: 1px solid black; - //padding: 8.5px; - font-size: 12.5px; - font-weight: bold; - - .propertiesView-settings-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-settings-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-settings-content { - margin-left: 12px; - padding-bottom: 10px; - padding-top: 8px; - } - } .propertiesView-sharing { //border-bottom: 1px solid black; //padding: 8.5px; - .propertiesView-sharing-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-sharing-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-sharing-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; .propertiesView-buttonContainer { float: right; @@ -133,16 +42,6 @@ padding: 0; margin-top: -5; } - - .propertiesView-acls-checkbox { - margin-top: -20px; - - .propertiesView-acls-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; - } - } } .change-buttons { @@ -157,6 +56,63 @@ width: 100%; } } + } + + .propertiesView-acls-checkbox { + float: right; + margin-left: 50px; + } + + .propertiesView-shareDropDown{ + margin-right: 10px; + min-width: 65px; + + & .propertiesView-shareDropDownNone{ + padding: 0px; + padding-left: 3px; + padding-right: 3px; + background: grey; + color: rgb(71, 71, 71); + border-radius: 6px; + border: 1px solid rgb(71, 71, 71); + } + & .propertiesView-shareDropDownEdit, + .propertiesView-shareDropDownAdmin{ + padding: 0px; + padding-left: 3px; + padding-right: 3px; + background: rgb(254, 254, 199); + color: rgb(75, 75, 5); + border-radius: 6px; + border: 1px solid rgb(75, 75, 5); + } + & .propertiesView-shareDropDownAugment{ + padding: 0px; + padding-left: 3px; + padding-right: 3px; + background: rgb(208, 255, 208); + color:rgb(19, 80, 19); + border-radius: 6px; + border: 1px solid rgb(19, 80, 19); + + } + & .propertiesView-shareDropDownView{ + padding: 0px; + padding-left: 3px; + padding-right: 3px; + background: rgb(213, 213, 255); + color: rgb(25, 25, 101); + border-radius: 6px; + border: 1px solid rgb(25, 25, 101); + } + & .propertiesView-shareDropDownNot-Shared{ + padding: 0px; + padding-left: 3px; + padding-right: 3px; + background: rgb(255, 207, 207); + color: rgb(138, 47, 47); + border-radius: 6px; + border: 1px solid rgb(138, 47, 47); } } @@ -164,33 +120,7 @@ //border-bottom: 1px solid black; //padding: 8.5px; - .propertiesView-filters-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-filters-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-filters-content { + .propertiesView-content { font-size: 10px; padding: 10px; margin-left: 5px; @@ -219,85 +149,10 @@ } } - .propertiesView-appearance { - //border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-appearance-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-appearance-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-appearance-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } - } - - .propertiesView-transform { - //border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-transform-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-transform-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-transform-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } - } - .notify-button { padding: 2px; width: 12px; height: 12px; - background-color: black; border-radius: 10px; padding-left: 2px; padding-right: 2px; @@ -317,19 +172,7 @@ } .expansion-button { - margin-left: -20; - - .expansion-button-icon { - width: 11px; - height: 11px; - color: black; - margin-left: 27px; - - &:hover { - color: rgb(131, 131, 131); - cursor: pointer; - } - } + margin-right: 10px; } .propertiesView-sharingTable { @@ -340,18 +183,13 @@ padding: 5px; // remove when adding buttons border-radius: 6px; // remove when adding buttons margin-right: 10px; // remove when adding buttons - // width: 100%; - // display: inline-table; background-color: #ececec; - max-height: 130px; - overflow-y: auto; - width: 92%; + width: 97%; .propertiesView-sharingTable-item { display: flex; - // padding: 5px; padding: 3px; - align-items: center; + align-items: right; border-bottom: 0.5px solid grey; &:hover .propertiesView-sharingTable-item-name { @@ -372,19 +210,9 @@ .propertiesView-sharingTable-item-permission { display: flex; align-items: flex-end; + text-align: right; margin-left: auto; - - .permissions-select { - border: none; - background-color: inherit; - width: 87px; - text-align: justify; // for Edge - text-align-last: end; - - &:hover { - cursor: pointer; - } - } + margin-right: -12px; } &:last-child { @@ -393,57 +221,9 @@ } } - .propertiesView-fields { - //border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-fields-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-fields-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-fields-checkbox { - float: right; - height: 20px; - margin-top: -9px; - - .propertiesView-fields-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; - } - } - - .propertiesView-fields-content { - font-size: 10px; - margin-left: 2px; - padding: 10px; - - &:hover { - cursor: pointer; - } - } + .propertiesView-permissions-select { + background: inherit; + border: none; } .propertiesView-field { @@ -466,109 +246,14 @@ } } - .propertiesView-contexts { - .propertiesView-contexts-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-contexts-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-contexts-content { - overflow: hidden; - padding: 10px; - } - } - - .propertiesView-layout { - .propertiesView-layout-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-layout-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-layout-content { - overflow: hidden; - padding: 10px; - } } .propertiesView-presTrails { //border-bottom: 1px solid black; //padding: 8.5px; - .propertiesView-presTrails-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-presTrails-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-presTrails-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } + } -} .inking-button { display: flex; @@ -830,8 +515,16 @@ } } +.propertiesView-wordTitle{ + color:darkslategray; + font-weight:200; +} + .editable-title { border: solid 1px #323232; + padding-left: 5px; + padding-top: 4px; + border-radius: 4px; height: fit-content; &:hover { @@ -839,6 +532,26 @@ } } +.propertiesView-wordType{ + color:darkslategray; + font-weight:200; +} + +.currentType{ + text-decoration: underline; + display: flex; + align-items:center; + // border: solid 1px #323232; + // padding-left: 5px; + // padding-top: 4px; + // border-radius: 4px; + // height: fit-content; +} + +.currentType-icon{ + margin-right:5px; +} + .properties-flyout { grid-column: 2/4; } @@ -847,10 +560,6 @@ padding: 5%; } -.propertiesView-section { - padding-left: 20px; -} - .propertiesView-input { padding: 4px 8px; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 19c138a21..872f1c6ab 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -3,37 +3,42 @@ 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 { intersection } from 'lodash'; -import { action, computed, Lambda, observable } from 'mobx'; +import { Button, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; +import { concat } from 'lodash'; +import { Lambda, action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, AclSym, DataSym, Doc, Field, FieldResult, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; +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'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; -import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; +import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util'; import { 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 { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager, undoBatch, undoable } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; -import { Colors } from './global/globalEnums'; 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'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -69,7 +74,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.selectedDoc?.type === DocumentType.LINK; } @computed get dataDoc() { - return this.selectedDoc?.[DataSym]; + return this.selectedDoc?.[DocData]; } @observable layoutFields: boolean = false; @@ -116,19 +121,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.selectedDoc?.type === DocumentType.INK; } - rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20)); - rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); + 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); @action docWidth = () => { if (this.selectedDoc) { const layoutDoc = this.selectedDoc; const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); - return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; - } else { - return 0; + if (aspect) return Math.min(layoutDoc[Width](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); + return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[Width](), this.props.width - 20) : this.props.width - 20; } + return 0; }; @action @@ -186,7 +190,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }); rows.push( - <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px' }}> + <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: 'white', textAlign: 'center' }}> <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} /> </div> ); @@ -199,13 +203,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get noviceFields() { - const noviceReqFields = ['author', 'creationDate', 'tags', '_layout_curPage']; + const noviceReqFields = ['author', 'author_date', 'tags', '_layout_curPage']; return this.editableFields(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields); } @undoBatch setKeyValue = (value: string) => { - const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); + const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc!] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => { if (value.indexOf(':') !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); @@ -220,7 +224,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const tags = StrListCast(doc.tags); if (!tags.includes(value)) { tags.push(value); - doc[DataSym].tags = tags.length ? new List<string>(tags) : undefined; + doc[DocData].tags = tags.length ? new List<string>(tags) : undefined; } return true; } @@ -244,11 +248,30 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return !this.selectedDoc ? null : <PropertiesDocContextSelector DocView={this.selectedDocumentView} hideTitle={true} addDocTab={this.props.addDocTab} />; } + @computed get contextCount() { + if (this.selectedDocumentView) { + const target = this.selectedDocumentView.props.Document; + const embeddings = DocListCast(target.proto_embeddings); + return embeddings.length - 1; + } else { + return 0; + } + } + @computed get links() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc; return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />; } + @computed get linkCount() { + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc; + var counter = 0; + + LinkManager.Links(selAnchor).forEach((l, i) => counter++); + + return counter; + } + @computed get layoutPreview() { if (SelectionManager.Views().length > 1) { return '-- multiple selected --'; @@ -276,8 +299,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { PanelHeight={panelHeight} focus={emptyFunction} ScreenToLocalTransform={this.getTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={returnFalse} moveDocument={undefined} @@ -287,7 +310,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { pinToPres={emptyFunction} bringToFront={returnFalse} dontRegisterView={true} - dropAction={undefined} /> </div> ); @@ -301,27 +323,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym])); - SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); + const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc! : DocCast(doc)[DocData])); + SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs, this.layoutDocAcls); }; /** * @returns the options for the permissions dropdown. */ - getPermissionsSelect(user: string, permission: string) { - const dropdownValues: string[] = Object.values(SharingPermissions); + getPermissionsSelect(user: string, permission: string, showGuestOptions: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); - if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return ( - <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {dropdownValues - .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)) - .map(permission => ( - <option key={permission} value={permission}> - {' '} - {permission}{' '} - </option> - ))} + <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> + {dropdownValues.map(permission => ( + <option className="propertiesView-permisssions-select" key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + ))} </select> ); } @@ -333,7 +351,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <Tooltip title={<div className="dash-tooltip">Notify with message</div>}> <div className="notify-button"> - <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" /> + <FontAwesomeIcon className="notify-button-icon" icon="bell" size="sm" /> </div> </Tooltip> ); @@ -344,17 +362,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @computed get expansionIcon() { return ( - <Tooltip title={<div className="dash-tooltip">Show more permissions</div>}> - <div - className="expansion-button" - onPointerDown={() => { + <div className="expansion-button"> + <IconButton + icon={<FontAwesomeIcon icon={'ellipsis-h'} />} + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + onClick={action(() => { if (this.selectedDocumentView || this.selectedDoc) { SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); } - }}> - <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> - </div> - </Tooltip> + })} + /> + </div> ); } @@ -362,6 +381,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { * @returns a row of the permissions panel */ sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { + if (name == Doc.CurrentUserEmail) { + name = 'Me'; + } return ( <div className="propertiesView-sharingTable-item" @@ -375,92 +397,208 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> - {admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : permission} - {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null} + {this.colorACLDropDown(name, admin, permission, false)} + {(permission === 'Owner' && name == 'Me') || showExpansionIcon ? this.expansionIcon : null} + </div> + </div> + ); + } + + /** + * @returns a colored dropdown bar reflective of the permission + */ + colorACLDropDown(name: string, admin: boolean, permission: string, showGuestOptions: boolean) { + var shareImage = ReverseHierarchyMap.get(permission)?.image; + return ( + <div> + <div className={'propertiesView-shareDropDown'}> + <div className={`propertiesView-shareDropDown${permission}`}> + <div>{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}</div> + </div> </div> </div> ); } /** + * Sorting algorithm to sort users. + */ + sortUsers = (u1: String, u2: String) => { + return u1 > u2 ? -1 : u1 === u2 ? 0 : 1; + }; + + /** + * Sorting algorithm to sort groups. + */ + sortGroups = (group1: Doc, group2: Doc) => { + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); + return g1 > g2 ? -1 : g1 === g2 ? 0 : 1; + }; + + /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { // all selected docs - const docs = - SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); - + const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views().map(docView => docView.rootDoc); const target = docs[0]; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); - const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); + const showAdmin = GetEffectiveAcl(target) == AclAdmin; + const individualTableEntries = []; + const usersAdded: string[] = []; // all shared users being added - organized by denormalized email + + const seldoc = this.layoutDocAcls || !this.selectedDoc ? this.selectedDoc : Doc.GetProto(this.selectedDoc); + // adds each user to usersAdded + SharingManager.Instance.users.forEach(eachUser => { + var userOnDoc = true; + if (seldoc) { + if (Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === '' || Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === undefined) { + userOnDoc = false; + } + } + if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email != target.author) { + usersAdded.push(eachUser.user.email); + } + }); - // users in common between all docs - const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); + // sorts and then adds each user to the table + usersAdded.sort(this.sortUsers); + usersAdded.map(userEmail => { + const userKey = `acl-${normalizeEmail(userEmail)}`; + var aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true); + var permission = StrCast(aclField); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user + }); - const tableEntries = []; + // adds current user + var userEmail = Doc.CurrentUserEmail; + if (userEmail == 'guest') userEmail = 'Guest'; + const userKey = `acl-${normalizeEmail(userEmail)}`; + if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && userEmail != target.author) { + var permission; + if (this.layoutDocAcls) { + if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; + else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]); + else permission = StrCast(Doc.GetProto(target)?.[userKey]); + } else permission = StrCast(target[userKey]); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user + } - // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (commonKeys.length) { - for (const key of commonKeys) { - const name = denormalizeEmail(key.substring(4)); - const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]); - if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-')); + // shift owner to top + individualTableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'), false); + + // adds groups + const groupTableEntries: JSX.Element[] = []; + const groupList = GroupManager.Instance?.allGroups || []; + groupList.sort(this.sortGroups); + groupList.map(group => { + if (group.title != 'Guest' && this.selectedDoc) { + const groupKey = 'acl-' + normalizeEmail(StrCast(group.title)); + if (this.selectedDoc[groupKey] != '' && this.selectedDoc[groupKey] != undefined) { + var permission; + if (this.layoutDocAcls) { + if (target[DocAcl][groupKey]) { + permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name; + } else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]); + else permission = StrCast(Doc.GetProto(target)?.[groupKey]); + } else permission = StrCast(target[groupKey]); + groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false)); } } - } + }); - const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); - // shifts the current user, owner, public to the top of the doc. - // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); - if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); - tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-')); - tableEntries.unshift( - this.sharingItem( - 'Me', - showAdmin, - docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', - !ownerSame - ) - ); + // guest permission + const guestPermission = StrCast((this.layoutDocAcls ? target : Doc.GetProto(target))['acl-Guest']); - return <div className="propertiesView-sharingTable">{tableEntries}</div>; + return ( + <div> + <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> {individualTableEntries}</div>} + </div> + {groupTableEntries.length > 0 ? ( + <div> + <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> {groupTableEntries}</div>} + </div> + </div> + ) : null} + <br></br> Guest + <div>{this.colorACLDropDown('Guest', showAdmin, guestPermission!, true)}</div> + </div> + ); } @computed get fieldsCheckbox() { + // color= "primary" return <Checkbox color="primary" onChange={this.toggleCheckbox} checked={this.layoutFields} />; } @action - toggleCheckbox = () => { - this.layoutFields = !this.layoutFields; - }; + toggleCheckbox = () => (this.layoutFields = !this.layoutFields); + + @computed get color() { + return StrCast(Doc.UserDoc().userColor); + } + + @computed get backgroundColor() { + return StrCast(Doc.UserDoc().userBackgroundColor); + } + + @computed get variantColor() { + return StrCast(Doc.UserDoc().userVariantColor); + } @computed get editableTitle() { const titles = new Set<string>(); - SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); 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 />; + } + + @computed get currentType() { + const documentType = StrCast(this.selectedDoc?.type); + var currentType: string = documentType; + var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType); + return ( - <div className="editable-title"> - <EditableView key="editableView" contents={title} height={25} fontSize={14} GetValue={() => title} SetValue={this.setTitle} /> + <div> + <div className="propertiesView-wordType">Type</div> + <div className="currentType"> + <div className="currentType-icon">{this.currentComponent}</div> + + {capitalizedDocType} + </div> </div> ); } + @computed get currentComponent() { + var iconName = StrCast(this.selectedDoc?.systemIcon); + + if (iconName) { + const Icon = Icons[iconName as keyof typeof Icons]; + return <Icon />; + } else { + return <Icons.BsFillCollectionFill />; + } + } + @undoBatch @action - setTitle = (value: string) => { + setTitle = (value: string | number) => { if (SelectionManager.Views().length > 1) { SelectionManager.Views().map(dv => Doc.SetInPlace(dv.rootDoc, 'title', value, true)); - return true; } else if (this.dataDoc) { if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true); - else KeyValueBox.SetField(this.dataDoc, 'title', value, true); - return true; + else KeyValueBox.SetField(this.dataDoc, 'title', value as string, true); } - return false; }; @undoBatch @@ -518,7 +656,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="inking-button-points" style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? 'black' : '' }} onPointerDown={action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton))}> - <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> + <FontAwesomeIcon icon="bezier-curve" size="lg" /> </div> </Tooltip> </div> @@ -534,23 +672,13 @@ 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} 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" color="white" size="sm" /> + <FontAwesomeIcon icon="caret-up" size="sm" /> </div> <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}> - <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> + <FontAwesomeIcon icon="caret-down" size="sm" /> </div> </div> </div> @@ -568,55 +696,50 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @action upDownButtons = (dirs: string, field: string) => { + const selDoc = this.selectedDoc; + if (!selDoc) return; + //prettier-ignore switch (field) { - case 'Xps': - this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10)); - break; - case 'Yps': - this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10)); - break; - case 'stk': - this.selectedDoc && (this.selectedDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1)); - break; + case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break; + case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break; + case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break; case 'wid': - const oldWidth = NumCast(this.selectedDoc?._width); - const oldHeight = NumCast(this.selectedDoc?._height); - const oldX = NumCast(this.selectedDoc?.x); - const oldY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10)); - const doc = this.selectedDoc; - if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - const ink = Cast(doc.data, InkField)?.inkData; + const oldWidth = NumCast(selDoc._width); + const oldHeight = NumCast(selDoc._height); + const oldX = NumCast(selDoc.x); + const oldY = NumCast(selDoc.y); + selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10); + if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { + const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = NumCast(doc.x) - oldX + (ink[j].X * NumCast(doc._width)) / oldWidth; - const newY = NumCast(doc.y) - oldY + (ink[j].Y * NumCast(doc._height)) / oldHeight; + const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth; + const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight; newPoints.push({ X: newX, Y: newY }); } - doc.data = new InkField(newPoints); + selDoc.data = new InkField(newPoints); } } break; case 'hgt': - const oWidth = NumCast(this.selectedDoc?._width); - const oHeight = NumCast(this.selectedDoc?._height); - const oX = NumCast(this.selectedDoc?.x); - const oY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10)); - const docu = this.selectedDoc; - if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { - const ink = Cast(docu.data, InkField)?.inkData; + const oWidth = NumCast(selDoc._width); + const oHeight = NumCast(selDoc._height); + const oX = NumCast(selDoc.x); + const oY = NumCast(selDoc.y); + selDoc._height = oHeight + (dirs === 'up' ? 10 : -10); + if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { + const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = NumCast(docu.x) - oX + (ink[j].X * NumCast(docu._width)) / oWidth; - const newY = NumCast(docu.y) - oY + (ink[j].Y * NumCast(docu._height)) / oHeight; + const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth; + const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight; newPoints.push({ X: newX, Y: newY }); } - docu.data = new InkField(newPoints); + selDoc.data = new InkField(newPoints); } } break; @@ -656,21 +779,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.inputBoxDuo( 'hgt', this.shapeHgt, - (val: string) => { - if (!isNaN(Number(val))) { - this.shapeHgt = val; - } - return true; - }, + undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height'), 'H:', 'wid', this.shapeWid, - (val: string) => { - if (!isNaN(Number(val))) { - this.shapeWid = val; - } - return true; - }, + undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width'), 'W:' ); } @@ -678,21 +791,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.inputBoxDuo( 'Xps', this.shapeXps, - (val: string) => { - if (val !== '0' && !isNaN(Number(val))) { - this.shapeXps = val; - } - return true; - }, + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord'), 'X:', 'Yps', this.shapeYps, - (val: string) => { - if (val !== '0' && !isNaN(Number(val))) { - this.shapeYps = val; - } - return true; - }, + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord'), 'Y:' ); } @@ -700,36 +803,24 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable private _fillBtn = false; @observable private _lineBtn = false; - private _lastFill = '#D0021B'; - private _lastLine = '#D0021B'; private _lastDash: any = '2'; @computed get colorFil() { - const ccol = this.getField('fillColor') || ''; - ccol && (this._lastFill = ccol); - return ccol; + return StrCast(this.selectedDoc?.fillColor); } @computed get colorStk() { - const ccol = this.getField('color') || ''; - ccol && (this._lastLine = ccol); - return ccol; + return StrCast(this.selectedDoc?.color); } set colorFil(value) { - value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } set colorStk(value) { - value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } - colorButton(value: string, type: string, setter: () => {}) { - // return <div className="properties-flyout" onPointerEnter={e => this.changeScrolling(false)} - // onPointerLeave={e => this.changeScrolling(true)}> - // <Flyout anchorPoint={anchorPoints.LEFT_TOP} - // content={type === "fill" ? this.fillPicker : this.linePicker}> + colorButton(value: string, type: string, setter: () => void) { return ( - <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> + <div className="color-button" key="color" onPointerDown={action(e => setter())}> <div className="color-button-preview" style={{ @@ -742,31 +833,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {value === '' || value === 'transparent' ? <p style={{ fontSize: 25, color: 'red', marginTop: -14 }}>☒</p> : ''} </div> ); - // </Flyout> - // </div>; } - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - }; - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - }; - - colorPicker(setter: (color: string) => {}, type: string) { + colorPicker(color: string, setter: (color: string) => void) { return ( <SketchPicker - onChange={type === 'stk' ? this.switchStk : this.switchFil} + onChange={undoable( + action((color: ColorState) => setter(color.hex)), + 'set stroke color property' + )} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={type === 'stk' ? this.colorStk : this.colorFil} + color={color} /> ); } @@ -775,22 +852,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.colorButton(this.colorFil, 'fill', () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; - return true; }); } @computed get lineButton() { return this.colorButton(this.colorStk, 'line', () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; - return true; }); } @computed get fillPicker() { - return this.colorPicker((color: string) => (this.colorFil = color), 'fil'); + return this.colorPicker(this.colorFil, (color: string) => (this.colorFil = color)); } @computed get linePicker() { - return this.colorPicker((color: string) => (this.colorStk = color), 'stk'); + return this.colorPicker(this.colorStk, (color: string) => (this.colorStk = color)); } @computed get strokeAndFill() { @@ -812,20 +887,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } - @computed get solidStk() { - return this.selectedDoc?.color && (!this.selectedDoc?.stroke_dash || this.selectedDoc?.stroke_dash === '0') ? true : false; - } @computed get dashdStk() { return this.selectedDoc?.stroke_dash || ''; } - @computed get unStrokd() { - return this.selectedDoc?.color ? true : false; - } @computed get widthStk() { return this.getField('stroke_width') || '1'; } @computed get markScal() { - return Number(this.getField('strokeMakerScale') || '1'); + return Number(this.getField('stroke_markerScale') || '1'); } @computed get markHead() { return this.getField('stroke_startMarker') || ''; @@ -833,23 +902,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get markTail() { return this.getField('stroke_endMarker') || ''; } - set solidStk(value) { - this.dashdStk = ''; - this.unStrokd = !value; - } set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); + value && (this._lastDash = value); this.selectedDoc && (this.selectedDoc.stroke_dash = value ? this._lastDash : undefined); } set markScal(value) { - this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); + this.selectedDoc && (this.selectedDoc.stroke_markerScale = Number(value)); } set widthStk(value) { this.selectedDoc && (this.selectedDoc.stroke_width = Number(value)); } - set unStrokd(value) { - this.colorStk = value ? '' : this._lastLine; - } set markHead(value) { this.selectedDoc && (this.selectedDoc.stroke_startMarker = value); } @@ -870,16 +932,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <input className="inputBox-input" type="text" value={value} 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" color="white" size="sm" /> + <FontAwesomeIcon icon="caret-up" size="sm" /> </div> <div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}> - <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> + <FontAwesomeIcon icon="caret-down" size="sm" /> </div> </div> </div> ); }; + @action + onDoubleClick = () => { + this.openContexts = false; + this.openLinks = false; + this.openOptions = false; + this.openTransform = false; + this.openFields = false; + this.openSharing = false; + this.openLayout = false; + this.openFilters = false; + }; + @computed get widthAndDash() { return ( <div className="widthAndDash"> @@ -959,195 +1033,177 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } + getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any) => { + return ( + <div> + <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 /> + </div> + ); + }; + @computed get transformEditor() { return ( <div className="transform-editor"> {this.isInk ? this.controlPointsButton : null} - {this.hgtInput} - {this.XpsInput} + {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') + )} </div> ); } @computed get optionsSubMenu() { return ( - <div className="propertiesView-settings" onPointerEnter={action(() => (this.inOptions = true))} onPointerLeave={action(() => (this.inOptions = false))}> - <div className="propertiesView-settings-title" onPointerDown={action(() => (this.openOptions = !this.openOptions))} style={{ backgroundColor: this.openOptions ? 'black' : '' }}> - Options - <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openOptions ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {!this.openOptions ? null : ( - <div className="propertiesView-settings-content"> - <PropertiesButtons /> - </div> - )} - </div> + <PropertiesSection + title="Options" + content={<PropertiesButtons />} + inSection={this.inOptions} + isOpen={this.openOptions} + setInSection={bool => (this.inOptions = bool)} + setIsOpen={bool => (this.openOptions = bool)} + onDoubleClick={() => this.onDoubleClick()} + /> ); } @computed get sharingSubMenu() { return ( - <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" onPointerDown={action(() => (this.openSharing = !this.openSharing))} style={{ backgroundColor: this.openSharing ? 'black' : '' }}> - Sharing {'&'} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {!this.openSharing ? null : ( - <div className="propertiesView-sharing-content"> - <div className="propertiesView-buttonContainer"> - {!Doc.noviceMode ? ( - <div className="propertiesView-acls-checkbox"> - <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> - <div className="propertiesView-acls-checkbox-text">Layout</div> - </div> - ) : null} - {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> - <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> - <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> - </button> - </Tooltip> */} + <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> + </Tooltip> */} + {/* </div> */} {this.sharingTable} - </div> - )} - </div> + </> + } + isOpen={this.openSharing} + setIsOpen={bool => (this.openSharing = bool)} + onDoubleClick={() => this.onDoubleClick()} + /> ); } /** - * Updates this.filterDoc's currentFilter and saves the docFilters on the currentFilter + * Updates this.filterDoc's currentFilter and saves the childFilters on the currentFilter */ updateFilterDoc = (doc: Doc) => { if (this.selectedDoc) { if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc - const savedDocFilters = doc._docFiltersList; - const currentDocFilters = this.selectedDoc._docFilters; - this.selectedDoc._docFilters = new List<string>(); - (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters; + const savedDocFilters = doc._childFiltersList; + const currentDocFilters = this.selectedDoc._childFilters; + this.selectedDoc._childFilters = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._childFiltersList = currentDocFilters; this.selectedDoc.currentFilter = doc; - doc._docFiltersList = new List<string>(); - this.selectedDoc._docFilters = savedDocFilters; + doc._childFiltersList = new List<string>(); + this.selectedDoc._childFilters = savedDocFilters; - const savedDocRangeFilters = doc._docRangeFiltersList; - const currentDocRangeFilters = this.selectedDoc._docRangeFilters; - this.selectedDoc._docRangeFilters = new List<string>(); - (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters; + const savedDocRangeFilters = doc._childFiltersByRangesList; + const currentDocRangeFilters = this.selectedDoc._childFiltersByRanges; + this.selectedDoc._childFiltersByRanges = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._childFiltersByRangesList = currentDocRangeFilters; this.selectedDoc.currentFilter = doc; - doc._docRangeFiltersList = new List<string>(); - this.selectedDoc._docRangeFilters = savedDocRangeFilters; + doc._childFiltersByRangesList = new List<string>(); + this.selectedDoc._childFiltersByRanges = savedDocRangeFilters; } }; @computed get filtersSubMenu() { return ( - <div className="propertiesView-filters"> - <div className="propertiesView-filters-title" onPointerDown={action(() => (this.openFilters = !this.openFilters))} style={{ backgroundColor: this.openFilters ? 'black' : '' }}> - Filters - <div className="propertiesView-filters-title-icon"> - <FontAwesomeIcon icon={this.openFilters ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {!this.openFilters ? null : ( - <div className="propertiesView-filters-content" style={{ position: 'relative', height: 'auto' }}> + <PropertiesSection + title="Filters" + content={ + <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}> <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} /> </div> - )} - </div> + } + isOpen={this.openFilters} + setIsOpen={bool => (this.openFilters = bool)} + onDoubleClick={() => this.onDoubleClick()} + /> ); } @computed get inkSubMenu() { + let isDouble = false; + return ( <> - {!this.isInk ? null : ( - <div className="propertiesView-appearance"> - <div className="propertiesView-appearance-title" onPointerDown={action(() => (this.openAppearance = !this.openAppearance))} style={{ backgroundColor: this.openAppearance ? 'black' : '' }}> - Appearance - <div className="propertiesView-appearance-title-icon"> - <FontAwesomeIcon icon={this.openAppearance ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {!this.openAppearance ? null : <div className="propertiesView-appearance-content">{this.appearanceEditor}</div>} - </div> - )} - - <div className="propertiesView-transform"> - <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}> - Transform - <div className="propertiesView-transform-title-icon"> - <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null} - </div> + <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()} /> </> ); } @computed get fieldsSubMenu() { return ( - <div className="propertiesView-fields"> - <div className="propertiesView-fields-title" onPointerDown={action(() => (this.openFields = !this.openFields))} style={{ backgroundColor: this.openFields ? 'black' : '' }}> - Fields {'&'} Tags - <div className="propertiesView-fields-title-icon"> - <FontAwesomeIcon icon={this.openFields ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {!Doc.noviceMode && this.openFields ? ( - <div className="propertiesView-fields-checkbox"> - {this.fieldsCheckbox} - <div className="propertiesView-fields-checkbox-text">Layout</div> - </div> - ) : null} - {!this.openFields ? null : <div className="propertiesView-fields-content">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>} - </div> + <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()} + /> ); } @computed get contextsSubMenu() { return ( - <div className="propertiesView-contexts"> - <div className="propertiesView-contexts-title" onPointerDown={action(() => (this.openContexts = !this.openContexts))} style={{ backgroundColor: this.openContexts ? 'black' : '' }}> - Other Contexts - <div className="propertiesView-contexts-title-icon"> - <FontAwesomeIcon icon={this.openContexts ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {this.openContexts ? <div className="propertiesView-contexts-content">{this.contexts}</div> : null} - </div> + <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()} + /> ); } @computed get linksSubMenu() { - return ( - <div className="propertiesView-contexts"> - <div className="propertiesView-contexts-title" onPointerDown={action(() => (this.openLinks = !this.openLinks))} style={{ backgroundColor: this.openLinks ? 'black' : '' }}> - Linked To - <div className="propertiesView-contexts-title-icon"> - <FontAwesomeIcon icon={this.openLinks ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {this.openLinks ? <div className="propertiesView-contexts-content">{this.links}</div> : null} - </div> - ); + 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()} />; } @computed get layoutSubMenu() { - return ( - <div className="propertiesView-layout"> - <div className="propertiesView-layout-title" onPointerDown={action(() => (this.openLayout = !this.openLayout))} style={{ backgroundColor: this.openLayout ? 'black' : '' }}> - Layout - <div className="propertiesView-layout-title-icon"> - <FontAwesomeIcon icon={this.openLayout ? 'caret-down' : 'caret-right'} size="lg" color="white" /> - </div> - </div> - {this.openLayout ? <div className="propertiesView-layout-content">{this.layoutPreview}</div> : null} - </div> - ); + return <PropertiesSection title="Layout" content={this.layoutPreview} isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={() => this.onDoubleClick()} />; } @computed get description() { @@ -1162,27 +1218,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_description = e.target.value; } // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_relationship = e.target.value; } - @undoBatch - handleDescriptionChange = action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.setDescripValue(value); - return true; - } - }); - - @undoBatch - handlelinkRelationshipChange = action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.setlinkRelationshipValue(value); - return true; - } - }); + handleDescriptionChange = undoable( + action((value: string) => { + if (LinkManager.currentLink && this.selectedDoc) { + this.setDescripValue(value); + } + }), + 'change link description' + ); + + handlelinkRelationshipChange = undoable( + action((value: string) => { + if (LinkManager.currentLink && this.selectedDoc) { + this.setlinkRelationshipValue(value); + } + }), + 'change link relationship' + ); @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { Doc.GetProto(LinkManager.currentLink).link_description = value; - return true; } }); @@ -1194,7 +1251,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { Doc.GetProto(LinkManager.currentLink).link_relationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes); - const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color if (!linkRelationshipList?.includes(value)) { @@ -1228,8 +1285,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }); - @undoBatch - changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow)); + changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior'); @undoBatch changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior)); @@ -1331,7 +1387,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { autoComplete="off" style={{ textAlign: 'left' }} id="link_description_input" - value={Field.toString(LinkManager.currentLink?.link_description as any as Field)} + value={StrCast(LinkManager.currentLink?.link_description)} onKeyDown={this.onDescriptionKey} onBlur={this.onSelectOutDesc} onChange={e => this.handleDescriptionChange(e.currentTarget.value)} @@ -1349,6 +1405,227 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale); }; + @computed get linkProperties() { + const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3)); + const targZoom = this.sourceAnchor?.followLinkZoom; + const indent = 30; + const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!); + + return ( + <> + <div className="propertiesView-section" style={{ background: 'darkgray' }}> + <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> + <p>Relationship</p> + {this.editRelationship} + </div> + <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> + <p>Description</p> + {this.editDescription} + </div> + <div className="propertiesView-input inline"> + <p>Show link</p> + <button + style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Auto-move anchors</p> + <button + style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Display arrow</p> + <button + style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + </div> + {!hasSelectedAnchor ? null : ( + <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)}> + <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> + <option value={OpenWhere.replaceLeft}>Replacing left tab</option> + <option value={OpenWhere.replaceRight}>Replacing right tab</option> + <option value={OpenWhere.lightbox}>Opening in lightbox</option> + <option value={OpenWhere.add}>Opening in new tab</option> + <option value={OpenWhere.replace}>Replacing current tab</option> + <option value={OpenWhere.inParent}>Opening in same collection</option> + {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} + </select> + </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')}> + <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()}> + {effect.toString()} + </option> + ))} + </select> + <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}> + {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} + {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} + {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} + {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} + {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} + </div> + </div> + {PresBox.inputter( + '0.1', + '0.1', + '10', + NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000, + true, + (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)), + indent + )}{' '} + <div + className={'slider-headers'} + style={{ + display: 'grid', + justifyContent: 'space-between', + width: `calc(100% - ${indent * 2}px)`, + marginLeft: indent, + marginRight: indent, + gridTemplateColumns: 'auto auto', + borderTop: 'solid', + }}> + <div className="slider-text">Fast</div> + <div className="slider-text">Slow</div> + </div>{' '} + <div className="propertiesView-input inline"> + <p>Play Target Audio</p> + <button + style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', 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 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', 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>Toggle Follow to Outer Context</p> + <button + style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Toggle Target (Show/Hide)</p> + <button + style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', 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>Ease Transitions</p> + <button + style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Capture Offset to Target</p> + <button + style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => { + this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined); + this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined); + }} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Center Target (no zoom)</p> + <button + style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', 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" 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} /> + <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'} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> + <FontAwesomeIcon icon={'caret-down'} /> + </div> + </div> + </div> + <button + style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} + <div + className={'slider-headers'} + style={{ + display: !targZoom ? 'none' : 'grid', + justifyContent: 'space-between', + width: `calc(100% - ${indent * 2}px)`, + marginLeft: indent, + marginRight: indent, + gridTemplateColumns: 'auto auto', + borderTop: 'solid', + }}> + <div className="slider-text">0%</div> + <div className="slider-text">100%</div> + </div>{' '} + </div> + )} + </> + ); + } + /** * Handles adding and removing members from the sharing panel */ @@ -1362,9 +1639,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { render() { const isNovice = Doc.noviceMode; - const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3)); - const targZoom = this.sourceAnchor?.followLinkZoom; - const indent = 30; const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!); if (!this.selectedDoc && !this.isPres) { return ( @@ -1380,260 +1654,49 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView" style={{ + background: StrCast(Doc.UserDoc().userBackgroundColor), + color: StrCast(Doc.UserDoc().userColor), width: this.props.width, minWidth: this.props.width, - //overflowY: this.scrolling ? "scroll" : "visible" }}> - <div className="propertiesView-title" style={{ width: this.props.width }}> - Properties + <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> </div> - <div className="propertiesView-name">{this.editableTitle}</div> + <div className="propertiesView-name">{this.editableTitle}</div> + <div className="propertiesView-type"> {this.currentType} </div> {this.contextsSubMenu} - {this.linksSubMenu} - {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : ( - <> - <div className="propertiesView-section" style={{ background: 'darkgray' }}> - <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> - <p>Relationship</p> - {this.editRelationship} - </div> - <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> - <p>Description</p> - {this.editDescription} - </div> - <div className="propertiesView-input inline"> - <p>Show link</p> - <button - style={{ background: !LinkManager.currentLink?.layout_linkDisplay ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'layout_linkDisplay')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Auto-move anchors</p> - <button - style={{ background: !LinkManager.currentLink?.layout_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'layout_autoMoveAnchors')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Display arrow</p> - <button - style={{ background: !LinkManager.currentLink?.layout_linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'layout_linkDisplayArrow')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - </div> - {!hasSelectedAnchor ? null : ( - <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)}> - <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> - <option value={OpenWhere.replaceLeft}>Replacing left tab</option> - <option value={OpenWhere.replaceRight}>Replacing right tab</option> - <option value={OpenWhere.fullScreen}>Overlaying current tab</option> - <option value={OpenWhere.lightbox}>Opening in lightbox</option> - <option value={OpenWhere.add}>Opening in new tab</option> - <option value={OpenWhere.replace}>Replacing current tab</option> - <option value={OpenWhere.inParent}>Opening in same collection</option> - {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} - </select> - </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')}> - <option value="default">Default</option> - {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( - <option value={effect.toString()}>{effect.toString()}</option> - ))} - </select> - <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}> - {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} - {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} - {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} - {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} - {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} - </div> - </div> - {PresBox.inputter( - '0.1', - '0.1', - '10', - NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000, - true, - (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)), - indent - )}{' '} - <div - className={'slider-headers'} - style={{ - display: 'grid', - justifyContent: 'space-between', - width: `calc(100% - ${indent * 2}px)`, - marginLeft: indent, - marginRight: indent, - gridTemplateColumns: 'auto auto', - borderTop: 'solid', - }}> - <div className="slider-text">Fast</div> - <div className="slider-text">Slow</div> - </div>{' '} - <div className="propertiesView-input inline"> - <p>Play Target Audio</p> - <button - style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', 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 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', 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>Toggle Follow to Outer Context</p> - <button - style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Toggle Target (Show/Hide)</p> - <button - style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', 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>Ease Transitions</p> - <button - style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Capture Offset to Target</p> - <button - style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => { - this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined); - this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined); - }} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Center Target (no zoom)</p> - <button - style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', 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" 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%' }} 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'} /> - </div> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> - <FontAwesomeIcon icon={'caret-down'} /> - </div> - </div> - </div> - <button - style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} - <div - className={'slider-headers'} - style={{ - display: !targZoom ? 'none' : 'grid', - justifyContent: 'space-between', - width: `calc(100% - ${indent * 2}px)`, - marginLeft: indent, - marginRight: indent, - gridTemplateColumns: 'auto auto', - borderTop: 'solid', - }}> - <div className="slider-text">0%</div> - <div className="slider-text">100%</div> - </div>{' '} - </div> - )} - </> - )} - + {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : this.linkProperties} {this.inkSubMenu} - {this.optionsSubMenu} - {this.fieldsSubMenu} - {isNovice ? null : this.sharingSubMenu} - {isNovice ? null : this.filtersSubMenu} - {isNovice ? null : this.layoutSubMenu} </div> ); } - if (this.isPres) { - const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0; + if (this.isPres && PresBox.Instance) { + const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0; const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType) ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType) : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type; return ( <div className="propertiesView" style={{ width: this.props.width }}> - <div className="propertiesView-title" style={{ width: this.props.width }}> + <div className="propertiesView-sectionTitle" style={{ width: this.props.width }}> Presentation </div> <div className="propertiesView-name" style={{ borderBottom: 0 }}> {this.editableTitle} <div className="propertiesView-presSelected"> - <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div> - <div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div> + <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div> + <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div> </div> </div> {!selectedItem ? null : ( @@ -1641,7 +1704,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Transitions <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null} @@ -1655,7 +1718,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Visibilty <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> {this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null} @@ -1666,7 +1729,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Progressivize <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null} @@ -1677,25 +1740,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'} <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> {this.openSlideOptions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null} </div> )} - {/* <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} - style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> - <FontAwesomeIcon icon={"plus"} /> Add new slide - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openAddSlide ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openAddSlide ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.newDocumentDropdown} - </div> : null} - </div> */} </div> ); } diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 5dfe10def..2bc2d5e6b 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 { undoable } from '../util/UndoManager'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @@ -161,7 +162,7 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss index a0506cb3a..d7de2b641 100644 --- a/src/client/views/SidebarAnnos.scss +++ b/src/client/views/SidebarAnnos.scss @@ -4,22 +4,26 @@ overflow: auto; flex-flow: row; flex-wrap: wrap; - .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active, - .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + .sidebarAnnos-filterTag, + .sidebarAnnos-filterTag-active, + .sidebarAnnos-filterUser, + .sidebarAnnos-filterUser-active { font-weight: bold; font-size: 10px; padding-left: 5; padding-right: 5; box-shadow: black 1px 1px 3px; border-radius: 5; - margin: 2; + margin: 2; height: 15; background-color: lightgrey; } - .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + .sidebarAnnos-filterUser, + .sidebarAnnos-filterUser-active { border-radius: 15px; } + .sidebarAnnos-filterUser-active, .sidebarAnnos-filterTag-active { background-color: white; } -}
\ No newline at end of file +} diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 42993bfc4..cd50865fb 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -49,17 +49,25 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { ); return keys; } + @computed get allHashtags() { + const keys = new Set<string>(); + DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag))); + return Array.from(keys.keys()) + .filter(key => key[0]) + .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase())) + .sort(); + } @computed get allUsers() { const keys = new Set<string>(); DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author))); return Array.from(keys.keys()).sort(); } get filtersKey() { - return '_' + this.sidebarKey + '-docFilters'; + return '_' + this.sidebarKey + '_childFilters'; } anchorMenuClick = (anchor: Doc) => { - const startup = StrListCast(this.props.rootDoc.docFilters) + const startup = StrListCast(this.props.rootDoc.childFilters) .map(filter => filter.split(':')[0]) .join(' '); const target = Docs.Create.TextDocument(startup, { @@ -69,15 +77,15 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { _height: 50, _layout_fitWidth: true, _layout_autoHeight: true, - _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), + _text_fontSize: StrCast(Doc.UserDoc().fontSize), + _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), }); FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' }); - link && (link.layout_linkDisplay = false); + link && (link.link_displayLine = false); - const taggedContent = this.docFilters() + const taggedContent = this.childFilters() .filter(data => data.split(':')[0]) .map(data => { const key = data.split(':')[0]; @@ -141,7 +149,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { return target; }; makeDocUnfiltered = (doc: Doc) => { - if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + 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>(); } @@ -169,7 +177,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); - docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; + childFilters = () => [...StrListCast(this.props.layoutDoc._childFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; layout_showTitle = () => 'title'; setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { @@ -179,9 +187,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }; render() { const renderTag = (tag: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`); return ( - <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, 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, this.sidebarKey, e.shiftKey)}> {tag} </div> ); @@ -216,6 +224,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }}> <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} + {this.allHashtags.map(renderTag)} {Array.from(this.allMetadata.keys()) .sort() .map(key => renderMeta(key, this.allMetadata.get(key)))} @@ -232,7 +241,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { ref={this._stackRef} PanelHeight={this.panelHeight} PanelWidth={this.panelWidth} - docFilters={this.docFilters} + childFilters={this.childFilters} sortFunc={this.sortByLinkAnchorY} setHeight={this.setHeightCallback} isAnnotationOverlay={false} @@ -248,7 +257,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { addDocument={this.addDocument} ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} + type_collection={CollectionViewType.Stacking} fieldKey={this.sidebarKey} pointerEvents={returnAll} /> diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 80c878386..c06bb287e 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -31,8 +31,8 @@ .styleProvider-treeView-icon, .styleProvider-treeView-icon-active { - margin-left: 0.25rem; - margin-right: 0.25rem; + margin-left: 0; + margin-right: 0; } .styleProvider-treeView-icon { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index e852d9297..63ff348e3 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { Shadows } from 'browndash-components'; +import { IconButton, Shadows, Size } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { extname } from 'path'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; @@ -18,13 +18,13 @@ import { TreeSort } from './collections/TreeView'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { MainView } from './MainView'; -import { DocumentViewProps, OpenWhere } from './nodes/DocumentView'; +import { 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 { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox'; export enum StyleProp { TreeViewIcon = 'treeViewIcon', @@ -37,14 +37,13 @@ export enum StyleProp { BackgroundColor = 'backgroundColor', // background color of a document view FillColor = 'fillColor', // fill color of an ink stroke or shape WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note - HideLinkButton = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button + HideLinkBtn = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that - showCaption = 'layout_showCaption', + ShowCaption = 'layout_showCaption', TitleHeight = 'titleHeight', // Height of Title area - layout_showTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix) - JitterRotation = 'jitterRotation', // whether documents should be randomly rotated + ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix) BorderPath = 'customBorder', // border path for document view FontSize = 'fontSize', // size of text font FontFamily = 'fontFamily', // font family of text @@ -84,20 +83,21 @@ export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab // export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any { - const remoteDocHeader = 'author;creationDate;noMargin'; + const remoteDocHeader = 'author;author_date;noMargin'; const docProps = testDocProps(props) ? props : undefined; const selected = property.includes(':selected'); const isCaption = property.includes(':caption'); const isAnchor = property.includes(':anchor'); const isAnnotated = property.includes(':annotated'); 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 opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); - const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.layout_showTitle); - const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); + const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); + // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: const img = ImageCast(doc?.icon, ImageCast(doc?.data)); @@ -106,17 +106,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const url = doc?.icon ? img.url.href : img.url.href.replace(ext, '_s' + ext); return <img src={url} width={20} height={15} style={{ margin: 'auto', display: 'block', objectFit: 'contain' }} />; } - return Doc.toIcon(doc, isOpen); - + return Doc.toIcon(doc, isEmpty ? undefined : isOpen); case StyleProp.TreeViewSortings: - const allSorts: { [key: string]: { color: string; label: string } | undefined } = {}; - allSorts[TreeSort.Down] = { color: 'blue', label: '↓' }; - allSorts[TreeSort.Up] = { color: 'crimson', label: '↑' }; - if (doc?._viewType === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' }; - allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; + const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {}; + allSorts[TreeSort.Down] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> }; + allSorts[TreeSort.Up] = { color: 'crimson', icon: <BsArrowUp/> }; + if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' }; + allSorts[TreeSort.None] = { color: 'darkgray', icon: <BsArrowDownUp/> }; return allSorts; case StyleProp.Highlighting: - if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) { + if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined; + 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]; @@ -131,21 +131,17 @@ 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.Opacity: - return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); - case StyleProp.HideLinkButton: - return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton); - case StyleProp.FontSize: - return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize))); - case StyleProp.FontFamily: - return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily))); - case StyleProp.FontWeight: - return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight))); - case StyleProp.layout_showTitle: + case StyleProp.DocContents:return undefined; + case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; + case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.textInlineAnnotations ? 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))); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._text_fontFamily, StrCast(Doc.UserDoc().fontFamily))); + case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._text_fontWeight, StrCast(Doc.UserDoc().fontWeight))); + case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, 'transparent')); + case StyleProp.ShowCaption:return doc?._type_collection === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption); + case StyleProp.TitleHeight:return 15; + case StyleProp.ShowTitle: return ( (doc && !doc.presentationTargetDoc && @@ -162,10 +158,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps )) || '' ); - case StyleProp.FillColor: - return Cast(doc?._fillColor, 'string', Cast(doc?.fillColor, 'string', 'transparent')); case StyleProp.Color: - if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY; + 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; const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color)); if (docColor) return docColor; const docView = props?.DocumentView?.(); @@ -173,9 +169,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (!backColor) return undefined; return lightOrDark(backColor); case StyleProp.BorderRounding: - return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : '')); - case StyleProp.TitleHeight: - return 15; + return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : '')); case StyleProp.BorderPath: const borderPath = Doc.IsComicStyle(doc) && props?.renderDepth && @@ -192,12 +186,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps </div> ), }; - case StyleProp.JitterRotation: - return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; - case StyleProp.showCaption: - return doc?._viewType === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption); case StyleProp.HeaderMargin: - return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || + return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) || (doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) && layout_showTitle() && @@ -205,108 +195,77 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? 15 : 0; case StyleProp.BackgroundColor: { - if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY; + if (MainView.Instance.LastButton === doc) return StrCast(Doc.UserDoc().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.PRESELEMENT: - docColor = docColor || (darkScheme() ? '' : ''); - break; - case DocumentType.PRES: - docColor = docColor || (darkScheme() ? 'transparent' : '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.FILTER: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : 'rgba(105, 105, 105, 0.432)'); - break; - case DocumentType.INK: - docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; - break; - case DocumentType.SLIDER: - 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.LINKANCHOR: - docColor = isAnchor ? Colors.LIGHT_BLUE : 'transparent'; - break; - case DocumentType.LINK: - docColor = (isAnchor ? docColor : '') || 'transparent'; - break; + case DocumentType.SLIDER: break; + case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? '' : ''); break; + case DocumentType.PRES: docColor = docColor || (darkScheme() ? 'transparent' : '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.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.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 || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; - docColor = - docColor || - (Doc.IsSystem(doc) - ? darkScheme() - ? Colors.DARK_GRAY - : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background + docColor = docColor || (Doc.IsSystem(doc) + ? SettingsManager.Instance.userBackgroundColor : doc.annotationOn ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup ? undefined - : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)')); - break; - //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - default: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); + : 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)); 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); } - if (docColor && !doc) docColor = DashColor(docColor).fade(0.5).toString(); - return docColor; + return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor; } case StyleProp.BoxShadow: { if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow) - if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW; + if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW; if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); switch (doc?.type) { case DocumentType.COL: return StrCast( - doc?.boxShadow, - doc?._viewType === CollectionViewType.Pile + doc?.layout_borderRounding, + 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.boxShadow, '0.2vw 0.2vw 0.8vw')}` + : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_borderRounding, '0.2vw 0.2vw 0.8vw')}` ); case DocumentType.LABEL: if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px'; default: return doc.z - ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow + ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow : props?.docViewPath().lastElement()?.rootDoc._freeform_useClusters - ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 - ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big - : StrCast(doc.boxShadow, ''); + : StrCast(doc.layout_boxShadow, ''); } } 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?.unrendered) return isInk ? 'visiblePainted' : 'all'; - if (doc?.pointerEvents) return StrCast(doc.pointerEvents); + if (MainView.Instance._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'; @@ -314,19 +273,19 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: const lock = () => { - if (props?.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform) { + if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) { return doc?.pointerEvents !== 'none' ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> - <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" /> + <FontAwesomeIcon icon='lock' style={{ color: 'red' }} size="lg" /> </div> ); } }; const filter = () => { const showFilterIcon = - StrListCast(doc?._docFilters).length || StrListCast(doc?._docRangeFilters).length + StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length ? '#18c718bd' //'hasFilter' - : docProps?.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.docRangeFilters().length + : docProps?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.childFiltersByRanges().length ? 'orange' //'inheritsFilter' : undefined; return !showFilterIcon ? null : ( @@ -359,18 +318,19 @@ 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); return ( - <div - title={field} - className={`styleProvider-treeView-icon${doc[field] ? '-active' : ''}`} + <IconButton + size={Size.XSMALL} + color={color} + icon={<FontAwesomeIcon icon={(doc[field] ? (onIcon as any) : offIcon) as IconProp} />} onClick={undoBatch( action((e: React.MouseEvent) => { e.stopPropagation(); clickFunc ? clickFunc() : (doc[field] = doc[field] ? undefined : true); }) - )}> - <FontAwesomeIcon icon={(doc[field] ? (onIcon as any) : offIcon) as IconProp} size="sm" /> - </div> + )} + /> ); } /** @@ -378,13 +338,9 @@ export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, */ export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { if (doc && property.split(':')[0] === StyleProp.Decorations) { - return doc._viewType === CollectionViewType.Docking ? null : ( - <> - {DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => { - DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)); - })} - </> - ); + return doc._type_collection === CollectionViewType.Docking || Doc.IsSystem(doc) + ? null + : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn))); } return DefaultStyleProvider(doc, props, property); } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 814a95ab8..5c2ab3f70 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -97,8 +97,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { TraceMobx(); const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layout_fieldKey, 'layout').replace('layout_', ''); - const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); + const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data); + const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data); const templateMenu: Array<JSX.Element> = []; this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />); @@ -116,13 +116,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { styleProvider={DefaultStyleProvider} setHeight={returnFalse} docViewPath={returnEmptyDoclist} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} rootSelected={returnFalse} onCheckedClick={this.scriptField} onChildClick={this.scriptField} - dropAction={undefined} isAnyChildContentActive={returnFalse} isContentActive={returnTrue} bringToFront={emptyFunction} diff --git a/src/client/views/UndoStack.scss b/src/client/views/UndoStack.scss index ab21e6d7e..192b99a12 100644 --- a/src/client/views/UndoStack.scss +++ b/src/client/views/UndoStack.scss @@ -2,9 +2,10 @@ height: 100%; display: flex; flex-direction: column; + justify-content: center; + align-items: center; position: relative; pointer-events: all; - padding-left: 4px; } .undoStack-resultContainer { @@ -21,9 +22,11 @@ } .undoStack-commandsContainer { - background-color: whitesmoke; flex: 1 1 auto; overflow-y: scroll; - height: 30px; + height: fit-content; + width: 200px; border-radius: 5px; + padding: 5px; + max-height: 200px; } diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx index f5af09e5b..a551e5332 100644 --- a/src/client/views/UndoStack.tsx +++ b/src/client/views/UndoStack.tsx @@ -3,6 +3,11 @@ 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'; interface UndoStackProps { width?: number; @@ -14,33 +19,39 @@ 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; return this.props.inline && UndoStack.HideInline ? null : ( - <div - className="undoStack-outerContainer" - style={{ width: this.props.width, height: this.props.height ? (UndoStack.Expand ? 4 : 1) * this.props.height : undefined, top: UndoStack.Expand && this.props.height ? -this.props.height * 3 : undefined }} - onClick={action(e => (UndoStack.Expand = !UndoStack.Expand))} - onDoubleClick={action(e => (UndoStack.Expand = UndoStack.HideInline = false))}> - <div className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}> - <div className="undoStack-resultContainer" key={0}> - <div className="undoStack-commandString" style={{ fontWeight: 'bold', textAlign: 'center' }}> - Undo/Redo Stack - </div> - </div> - {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(/[^\.]*\./, '')} + <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> - </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(/[^\.]*\./, '')} + </div> + </div> + ))} + </div> + } + /> </div> ); } diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 3465a5283..addc00c85 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -123,7 +123,7 @@ export type RegionData = makeInterface<[typeof RegionDataSchema]>; export const RegionData = makeInterface(RegionDataSchema); interface IProps { - node: Doc; + animatedDoc: Doc; RegionData: Doc; collection: Doc; tickSpacing: number; @@ -167,7 +167,7 @@ export class Keyframe extends React.Component<IProps> { return RegionData(this.props.RegionData); } @computed private get regions() { - return DocListCast(this.props.node.regions); + return DocListCast(this.props.animatedDoc.regions); } @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); @@ -375,7 +375,7 @@ export class Keyframe extends React.Component<IProps> { */ @action makeRegionMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), + TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.animatedDoc.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => { runInAction(() => { let cannotMove: boolean = false; @@ -461,7 +461,7 @@ export class Keyframe extends React.Component<IProps> { e.stopPropagation(); const div = ref.current!; div.style.opacity = '1'; - Doc.BrushDoc(this.props.node); + Doc.BrushDoc(this.props.animatedDoc); }; /** @@ -473,7 +473,7 @@ export class Keyframe extends React.Component<IProps> { e.stopPropagation(); const div = ref.current!; div.style.opacity = '0'; - Doc.UnBrushDoc(this.props.node); + Doc.UnBrushDoc(this.props.animatedDoc); }; ///////////////////////UI STUFF ///////////////////////// diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index adc97bbb4..7ca13756a 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -535,7 +535,7 @@ export class Timeline extends React.Component<FieldViewProps> { {this.children.map(doc => ( <Track ref={ref => this.mapOfTracks.push(ref)} - node={doc} + animatedDoc={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 2349ba786..1010332f5 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -12,7 +12,7 @@ import { Keyframe, KeyframeFunc, RegionData } from './Keyframe'; import './Track.scss'; interface IProps { - node: Doc; + animatedDoc: Doc; currentBarX: number; transform: Transform; collection: Doc; @@ -36,23 +36,23 @@ export class Track extends React.Component<IProps> { private objectWhitelist = ['data']; @computed private get regions() { - return DocListCast(this.props.node.regions); + return DocListCast(this.props.animatedDoc.regions); } @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); } async componentDidMount() { - const regions = await DocListCastAsync(this.props.node.regions); - if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff + const regions = await DocListCastAsync(this.props.animatedDoc.regions); + if (!regions) this.props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff //these two lines are exactly same from timeline.tsx const relativeHeight = window.innerHeight / 20; runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness this._timelineVisibleReaction = this.timelineVisibleReaction(); this._currentBarXReaction = this.currentBarXReaction(); - if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time); - this.props.node.hidden = false; - this.props.node.opacity = 1; + if (DocListCast(this.props.animatedDoc.regions).length === 0) this.createRegion(this.time); + this.props.animatedDoc.hidden = false; + this.props.animatedDoc.opacity = 1; // this.autoCreateKeyframe(); } @@ -127,13 +127,13 @@ export class Track extends React.Component<IProps> { */ @action autoCreateKeyframe = () => { - const objects = this.objectWhitelist.map(key => this.props.node[key]); - intercept(this.props.node, change => { + const objects = this.objectWhitelist.map(key => this.props.animatedDoc[key]); + intercept(this.props.animatedDoc, change => { return change; }); return reaction( () => { - return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects]; + return [...this.primitiveWhitelist.map(key => this.props.animatedDoc[key]), ...objects]; }, (changed, reaction) => { //check for region @@ -171,14 +171,14 @@ export class Track extends React.Component<IProps> { () => { const regiondata = this.findRegion(this.time); if (regiondata) { - this.props.node.hidden = false; + this.props.animatedDoc.hidden = false; // if (!this._autoKfReaction) { // // this._autoKfReaction = this.autoCreateKeyframe(); // } this.timeChange(); } else { - this.props.node.hidden = true; - this.props.node.opacity = 0; + this.props.animatedDoc.hidden = true; + this.props.animatedDoc.opacity = 0; //if (this._autoKfReaction) this._autoKfReaction(); } } @@ -250,10 +250,10 @@ export class Track extends React.Component<IProps> { private applyKeys = async (kf: Doc) => { this.primitiveWhitelist.forEach(key => { if (!kf[key]) { - this.props.node[key] = undefined; + this.props.animatedDoc[key] = undefined; } else { const stored = kf[key]; - this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; + this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored; } }); }; @@ -282,11 +282,11 @@ export class Track extends React.Component<IProps> { const dif = NumCast(right[key]) - NumCast(left[key]); const deltaLeft = this.time - NumCast(left.time); const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time)); - this.props.node[key] = NumCast(left[key]) + dif * ratio; + this.props.animatedDoc[key] = NumCast(left[key]) + dif * ratio; } else { // case data const stored = left[key]; - this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; + this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored; } }); }; @@ -326,7 +326,7 @@ export class Track extends React.Component<IProps> { regiondata.duration = rightRegion.position - regiondata.position; } if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) { - Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata); + Cast(this.props.animatedDoc.regions, listSpec(Doc))?.push(regiondata); this._newKeyframe = true; this.saveStateRegion = regiondata; return regiondata; @@ -360,7 +360,7 @@ export class Track extends React.Component<IProps> { @action copyDocDataToKeyFrame = (doc: Doc) => { this.primitiveWhitelist.map(key => { - const originalVal = this.props.node[key]; + const originalVal = this.props.animatedDoc[key]; doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; }); }; @@ -377,8 +377,8 @@ export class Track extends React.Component<IProps> { ref={this._inner} style={{ height: `${this._trackHeight}px` }} onDoubleClick={this.onInnerDoubleClick} - onPointerOver={() => Doc.BrushDoc(this.props.node)} - onPointerOut={() => Doc.UnBrushDoc(this.props.node)}> + onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)} + onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}> {this.regions?.map((region, i) => { return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />; })} diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 5c8b491eb..6bd1d9f5f 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -1,3 +1,4 @@ +@import '../global/globalCssVariables'; .collectionCarousel3DView-outer { height: 100%; position: relative; @@ -7,8 +8,8 @@ .carousel-wrapper { display: flex; position: absolute; - top: 15%; - height: 60%; + top: $CAROUSEL3D_TOP * 1%; + height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); @@ -17,10 +18,17 @@ flex: 1; transition: opacity 0.3s linear, transform 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955); pointer-events: none; + opacity: 0.5; + z-index: 1; + transform: scale($CAROUSEL3D_SIDE_SCALE); + user-select: none; } .collectionCarousel3DView-item-active { pointer-events: unset; + opacity: 1; + z-index: 2; + transform: scale($CAROUSEL3D_CENTER_SCALE); } } @@ -105,4 +113,4 @@ .carousel3DView-back-scroll:hover, .carousel3DView-fwd-scroll:hover { background: lightgray; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index a266c9207..d94e552b4 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -7,6 +7,8 @@ import { Id } from '../../../fields/FieldSymbols'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { returnFalse, returnZero, Utils } from '../../../Utils'; 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 { StyleProp } from '../StyleProvider'; import './CollectionCarousel3DView.scss'; @@ -32,27 +34,34 @@ export class CollectionCarousel3DView extends CollectionSubView() { } }; + centerScale = Number(CAROUSEL3D_CENTER_SCALE); panelWidth = () => this.props.PanelWidth() / 3; - panelHeight = () => this.props.PanelHeight() * 0.6; + panelHeight = () => this.props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE); onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); isChildContentActive = () => (this.isContentActive() ? true : false); + childScreenToLocal = () => + this.props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1) + .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor. + .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); @computed get content() { - const currentIndex = NumCast(this.layoutDoc._itemIndex); + const currentIndex = NumCast(this.layoutDoc._carousel_index); const displayDoc = (childPair: { layout: Doc; data: Doc }) => { return ( <DocumentView {...this.props} NativeWidth={returnZero} NativeHeight={returnZero} - suppressSetHeight={true} + //suppressSetHeight={true} onDoubleClick={this.onChildDoubleClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} Document={childPair.layout} DataDoc={childPair.data} + ScreenToLocalTransform={this.childScreenToLocal} isContentActive={this.isChildContentActive} isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} @@ -64,10 +73,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.childLayoutPairs.map((childPair, index) => { return ( - <div - key={childPair.layout[Id]} - className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} - style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}> + <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> {displayDoc(childPair)} </div> ); @@ -75,7 +81,8 @@ export class CollectionCarousel3DView extends CollectionSubView() { } changeSlide = (direction: number) => { - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; + SelectionManager.DeselectAll(); + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; }; onArrowClick = (e: React.MouseEvent, direction: number) => { @@ -114,10 +121,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { 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'} /> + <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'} /> + <FontAwesomeIcon icon="angle-right" size={'2x'} /> </div> {this.autoScrollButton} </div> @@ -139,13 +146,14 @@ export class CollectionCarousel3DView extends CollectionSubView() { } @computed get dots() { - return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />); + return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />); + } + @computed get translateX() { + const index = NumCast(this.layoutDoc._carousel_index); + return this.panelWidth() * (1 - index); } render() { - const index = NumCast(this.layoutDoc._itemIndex); - const translateX = this.panelWidth() * (1 - index); - return ( <div className="collectionCarousel3DView-outer" @@ -154,7 +162,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), }}> - <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}> + <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}> {this.content} </div> {this.props.Document._chromeHidden ? null : this.buttons} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 9ec78155c..ea02bcd4c 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -30,11 +30,11 @@ export class CollectionCarouselView extends CollectionSubView() { advance = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length; }; goback = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; }; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<DocumentViewProps>, property: string): any => { // first look for properties on the document in the carousel, then fallback to properties on the container @@ -45,7 +45,7 @@ export class CollectionCarouselView extends CollectionSubView() { onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { - const index = NumCast(this.layoutDoc._itemIndex); + 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']); @@ -60,6 +60,8 @@ export class CollectionCarouselView extends CollectionSubView() { NativeHeight={returnZero} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} + isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.props.isContentActive} + isContentActive={this.props.childContentsActive ?? this.props.isContentActive() === false ? returnFalse : emptyFunction} hideCaptions={show_captions ? true : false} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 78e44dfa2..d93015506 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -15,6 +15,12 @@ cursor: grab; color: $black; } +.collectiondockingview-container .lm_splitter { + opacity: 0.2; + &:hover { + opacity: 1; + } +} .lm_title.focus-visible { -webkit-appearance: none; @@ -89,6 +95,13 @@ position: relative; } +.lm_maximised .lm_header { + background-color: #000000; +} +.lm_maximised .lm_tab { + width: 100%; +} + .lm_stack { position: relative; } @@ -163,6 +176,7 @@ display: flex; align-content: center; justify-content: center; + background: $dark-gray; } .lm_controls > li { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 3a4691484..0052c4196 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -6,9 +6,9 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { inheritParentAcls } from '../../../fields/util'; +import { GetEffectiveAcl, inheritParentAcls } from '../../../fields/util'; import { emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; @@ -20,14 +20,17 @@ import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; import { LightboxView } from '../LightboxView'; +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { UndoStack } from '../UndoStack'; 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 { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; -import { OverlayView } from '../OverlayView'; -import { ScriptingRepl } from '../ScriptingRepl'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -85,6 +88,7 @@ export class CollectionDockingView extends CollectionSubView() { tabItemDropped = () => DragManager.CompleteWindowDrag?.(false); tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => { + this._flush = this._flush ?? UndoManager.StartBatch('tab move'); const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc; dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc])); DragManager.CompleteWindowDrag = (aborted: boolean) => { @@ -92,12 +96,12 @@ export class CollectionDockingView extends CollectionSubView() { proxy._dragListener.AbortDrag(); if (this._flush) { this._flush.cancel(); // cancel the undo change being logged - this._flush = undefined; this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout) } DragManager.CompleteWindowDrag = undefined; } finishDrag?.(aborted); + setTimeout(this.endUndoBatch, 100); }; }; @undoBatch @@ -123,28 +127,6 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch - public static OpenFullScreen(doc: Doc) { - SelectionManager.DeselectAll(); - const instance = CollectionDockingView.Instance; - if (instance) { - if (doc._viewType === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') { - return DashboardView.openDashboard(doc); - } - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(Doc.MakeEmbedding(doc))], - }; - const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - instance._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - instance._goldenLayout._$maximiseItem(docconfig); - instance._goldenLayout.emit('stateChanged'); - instance.stateChanged(); - } - return true; - } - - @undoBatch @action public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean, keyValue?: boolean): boolean { const instance = CollectionDockingView.Instance; @@ -172,20 +154,17 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { - return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 - ? CollectionDockingView.CloseSplit(doc) - : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue); + return Array.from(CollectionDockingView.Instance?.tabMap.keys() ?? []).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue); } // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // - @undoBatch @action public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { - if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); + if (document?._type_collection === CollectionViewType.Docking && !keyValue) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; - const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue); + const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !tab.contentItem.config.props.keyValue && !keyValue); if (tab) { tab.header.parent.setActiveContentItem(tab.contentItem); return true; @@ -195,6 +174,8 @@ export class CollectionDockingView extends CollectionSubView() { if (!instance) return false; const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue); + CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split'); + setTimeout(CollectionDockingView.Instance.endUndoBatch, 100); if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1])); @@ -358,6 +339,7 @@ export class CollectionDockingView extends CollectionSubView() { } catch (e) {} this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); + window.removeEventListener('mouseup', this.onPointerUp); this._reactionDisposer?.(); this._lightboxReactionDisposer?.(); @@ -370,16 +352,35 @@ export class CollectionDockingView extends CollectionSubView() { !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); }; + endUndoBatch = () => { + const json = JSON.stringify(this._goldenLayout.toConfig()); + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')); + const docs = !docids + ? [] + : docids + .map(id => DocServer.GetCachedRefField(id)) + .filter(f => f) + .map(f => f as Doc); + const changesMade = this.props.Document.dockingConfig !== json; + if (changesMade) { + if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) { + this.layoutDoc.dockingConfig = json; + this.layoutDoc.data = new List<Doc>(docs); + } else { + Doc.SetInPlace(this.rootDoc, 'dockingConfig', json, true); + Doc.SetInPlace(this.rootDoc, 'data', new List<Doc>(docs), true); + } + } + this._flush?.end(); + this._flush = undefined; + }; + @action onPointerUp = (e: MouseEvent): void => { window.removeEventListener('pointerup', this.onPointerUp); - const flush = this._flush; - this._flush = undefined; - if (flush) { - DragManager.CompleteWindowDrag = undefined; - if (!this.stateChanged()) flush.cancel(); - else flush.end(); - } + DragManager.CompleteWindowDrag = undefined; + setTimeout(this.endUndoBatch, 100); }; @action @@ -393,9 +394,14 @@ export class CollectionDockingView extends CollectionSubView() { window.addEventListener('mouseup', this.onPointerUp); if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; - if (!className.includes('lm_close') && !className.includes('lm_maximise')) { - this._flush = UndoManager.StartBatch('golden layout edit'); - DocServer.UPDATE_SERVER_CACHE(); + if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize'); + else { + const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement); + const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget); + if (map?.DashDoc && DocumentManager.Instance.getFirstDocumentView(map.DashDoc)) { + SelectionManager.SelectView(DocumentManager.Instance.getFirstDocumentView(map.DashDoc), false); + } + if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE(); } } } @@ -438,7 +444,7 @@ export class CollectionDockingView extends CollectionSubView() { const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc)); if (newtabdocs.length) { Doc.GetProto(newtab).data = new List<Doc>(newtabdocs); - newtabdocs.forEach(ntab => (ntab.embedContainer = newtab)); + newtabdocs.forEach(ntab => Doc.SetContainer(ntab, newtab)); } json = json.replace(origtab[Id], newtab[Id]); return newtab; @@ -452,28 +458,18 @@ export class CollectionDockingView extends CollectionSubView() { stateChanged = () => { this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); const json = JSON.stringify(this._goldenLayout.toConfig()); - const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); - const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')); - const docs = !docids - ? [] - : docids - .map(id => DocServer.GetCachedRefField(id)) - .filter(f => f) - .map(f => f as Doc); const changesMade = this.props.Document.dockingConfig !== json; - if (changesMade && !this._flush) { - UndoManager.RunInBatch(() => { - this.props.Document.dockingConfig = json; - this.props.Document.data = new List<Doc>(docs); - }, 'state changed'); - } return changesMade; }; tabDestroyed = (tab: any) => { - if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { + this._flush = this._flush ?? UndoManager.StartBatch('tab movement'); + if (tab.DashDoc && ![DocumentType.PRES].includes(tab.DashDoc?.type) && !tab.contentItem.config.props.keyValue) { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); - Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); + // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed + if (tab.DashDoc.embedContainer === this.rootDoc) tab.DashDoc.embedContainer = undefined; + if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); + Doc.RemoveDocFromList(Doc.GetProto(tab.DashDoc), 'proto_embeddings', tab.DashDoc); } if (CollectionDockingView.Instance) { const dview = CollectionDockingView.Instance.props.Document; @@ -502,7 +498,7 @@ export class CollectionDockingView extends CollectionSubView() { _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + inheritParentAcls(this.rootDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); @@ -545,7 +541,7 @@ export class CollectionDockingView extends CollectionSubView() { _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + inheritParentAcls(this.dataDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) @@ -586,8 +582,12 @@ ScriptingGlobals.add( case OpenWhere.addRight: return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); case OpenWhere.overlay: - if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); - else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + // prettier-ignore + switch (doc) { + case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); + case "<UndoStack>": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); + } + Doc.AddToMyOverlay(doc); } }, 'opens up document in location specified', diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 528781991..06522b85e 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -2,12 +2,12 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; -import { NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, numberRange, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, numberRange, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { CompileScript } from '../../util/Scripting'; @@ -97,14 +97,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createEmbeddingSelected = false; if (de.complete.docDragData) { - this.props.parent.Document.dropConverter instanceof ScriptField && this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); const key = this.props.pivotField; const castedValue = this.getValue(this.heading); const onLayoutDoc = this.onLayoutDoc(key); - de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); - this.props.parent.onInternalDrop(e, de); - e.stopPropagation(); + if (this.props.parent.onInternalDrop(e, de)) { + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); + } + return true; } + return false; }); getValue = (value: string): any => { @@ -156,7 +157,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const onLayoutDoc = this.onLayoutDoc(key); FormattedTextBox.SelectOnLoad = newDoc[Id]; FormattedTextBox.SelectOnLoadChar = value; - (onLayoutDoc ? newDoc : newDoc[DataSym])[key] = this.getValue(this.props.heading); + (onLayoutDoc ? newDoc : newDoc[DocData])[key] = this.getValue(this.props.heading); const docs = this.props.parent.childDocList; return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) }; @@ -250,10 +251,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr return ( <div className="collectionStackingView-optionPicker"> <div className="optionOptions"> - <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.toggleEmbedding}> - Create Embedding - </div> - <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.deleteRow}> + <div className={'optionPicker' + (selected === true ? ' active' : '')} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}> Delete </div> </div> @@ -271,13 +269,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; return this.collapsed ? null : ( <div style={{ position: 'relative' }}> + {this.props.showHandle && this.props.parent.props.isContentActive() ? this.props.parent.columnDragger : null} {showChrome ? ( <div className="collectionStackingView-addDocumentButton" - style={{ - //width: style.columnWidth / style.numGroupColumns, - padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px`, - }}> + style={ + { + //width: style.columnWidth / style.numGroupColumns, + //padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px`, + } + }> <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents={'+ NEW'} /> </div> ) : null} @@ -286,12 +287,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr ref={this._contRef} style={{ padding: stackPad, + minHeight: this.props.showHandle && this.props.parent.props.isContentActive() ? '10px' : undefined, width: this.props.parent.NodeWidth, gridGap: this.props.parent.gridGap, gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ''), }}> {this.props.parent.children(this.props.docList)} - {this.props.showHandle && this.props.parent.props.isContentActive() ? this.props.parent.columnDragger : null} </div> </div> ); @@ -311,23 +312,33 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr onPointerDown={this.headerDown} title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''} style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this.color : 'lightgrey' }}> - {noChrome ? evContents : editableHeaderView} + {noChrome ? evContents : <div>{editableHeaderView}</div>} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionColor"> - <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}> + <button + className="collectionStackingView-sectionColorButton" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + action(e => (this._paletteOn = !this._paletteOn)) + ) + }> <FontAwesomeIcon icon="palette" size="lg" /> </button> {this._paletteOn ? this.renderColorPicker() : null} </div> )} {noChrome ? null : ( - <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}> + <button className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}> <FontAwesomeIcon icon={this.collapsed ? 'chevron-down' : 'chevron-up'} size="lg" /> </button> )} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( - <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderMenu()}> + <div className="collectionStackingView-sectionOptions" onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> <FontAwesomeIcon icon="ellipsis-v" size="lg" /> </button> diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index c35f088a6..6eeccc94e 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -8,652 +8,15 @@ background-color: $dark-gray; height: 35px; border-bottom: $standard-border; - padding-right: 5px; + padding: 0 10px; align-items: center; + overflow-x: scroll; + &::-webkit-scrollbar { + display: none; + } - .collectionMenu-hardCodedButton { - cursor: pointer; - color: $white; - width: 25px; - height: 25px; - padding: 5; - text-align: center; + .hardCodedButtons { display: flex; - justify-content: center; - align-items: center; - position: relative; - transition: 0.2s; - border-radius: 3px; - - &:hover { - background-color: rgba(0, 0, 0, 0.2); - } + flex-direction: row; } -} - -// .collectionMenu-cont { -// position: relative; -// display: inline-flex; -// width: 100%; -// opacity: 0.9; -// z-index: 901; -// transition: top .5s; -// background: $dark-gray; -// color: $white; -// transform-origin: top left; -// top: 0; -// width: 100%; - -// .recordButtonOutline { -// border-radius: 100%; -// width: 18px; -// height: 18px; -// border: solid 1px $white; -// display: flex; -// align-items: center; -// justify-content: center; -// } - -// .recordButtonInner { -// border-radius: 100%; -// width: 70%; -// height: 70%; -// background: $white; -// } - -// .collectionMenu { -// display: flex; -// height: 100%; -// overflow: visible; -// z-index: 901; -// border: unset; - -// .collectionMenu-divider { -// height: 100%; -// margin-left: 3px; -// margin-right: 3px; -// width: 2px; -// background-color: $medium-gray; -// } - -// .collectionViewBaseChrome { -// display: flex; -// align-items: center; - -// .collectionViewBaseChrome-viewPicker { -// font-size: $small-text; -// outline-color: $black; -// color: $white; -// border: none; -// background: $dark-gray; -// } - -// .collectionViewBaseChrome-viewPicker:focus { -// outline: none; -// border: none; -// } - -// .collectionViewBaseChrome-viewPicker:active { -// outline-color: $black; -// } - -// .collectionViewBaseChrome-button { -// font-size: $small-text; -// text-transform: uppercase; -// letter-spacing: 2px; -// background: $white; -// color: $pink; -// outline-color: $black; -// border: none; -// padding: 12px 10px 11px 10px; -// margin-left: 10px; -// } - -// .collectionViewBaseChrome-cmdPicker { -// margin-left: 3px; -// margin-right: 0px; -// font-size: $small-text; -// text-transform: capitalize; -// color: $white; -// border: none; -// background: $dark-gray; -// } - -// .collectionViewBaseChrome-cmdPicker:focus { -// border: none; -// outline: none; -// } - -// .commandEntry-outerDiv { -// pointer-events: all; -// background-color: transparent; -// display: flex; -// flex-direction: row; -// align-items: center; -// justify-content: center; -// height: 100%; -// overflow: hidden; - -// .commandEntry-drop { -// color: $white; -// width: 30px; -// margin-top: auto; -// margin-bottom: auto; -// } -// } - -// .commandEntry-outerDiv:hover{ -// background-color: $drop-shadow; - -// .collectionViewBaseChrome-viewPicker, -// .collectionViewBaseChrome-cmdPicker{ -// background: $dark-gray; -// } -// } - -// .collectionViewBaseChrome-collapse { -// transition: all .5s, opacity 0.3s; -// position: absolute; -// width: 30px; -// transform-origin: top left; -// pointer-events: all; -// // margin-top: 10px; -// } - -// @media only screen and (max-device-width: 480px) { -// .collectionViewBaseChrome-collapse { -// display: none; -// } -// } - -// .collectionViewBaseChrome-template, -// .collectionViewBaseChrome-viewModes { -// align-items: center; -// height: 100%; -// display: flex; -// background: transparent; -// color: $medium-gray; -// justify-content: center; -// } - -// .collectionViewBaseChrome-viewSpecs { -// margin-left: 5px; -// display: grid; -// border: none; -// border-right: solid $medium-gray 1px; - -// .collectionViewBaseChrome-filterIcon { -// position: relative; -// display: flex; -// margin: auto; -// background: $dark-gray; -// color: $white; -// width: 30px; -// height: 30px; -// align-items: center; -// justify-content: center; -// border: none; -// border-right: solid $medium-gray 1px; -// } - -// .collectionViewBaseChrome-viewSpecsInput { -// padding: 12px 10px 11px 10px; -// border: 0px; -// color: $medium-gray; -// text-align: center; -// letter-spacing: 2px; -// outline-color: $black; -// font-size: $small-text; -// background: $white; -// height: 100%; -// width: 75px; -// } - -// .collectionViewBaseChrome-viewSpecsMenu { -// overflow: hidden; -// transition: height .5s, display .5s; -// position: absolute; -// top: 60px; -// z-index: 100; -// display: flex; -// flex-direction: column; -// background: $white; -// box-shadow: $medium-gray 2px 2px 4px; - -// .qs-datepicker { -// left: unset; -// right: 0; -// } - -// .collectionViewBaseChrome-viewSpecsMenu-row { -// display: grid; -// grid-template-columns: 150px 200px 150px; -// margin-top: 10px; -// margin-right: 10px; - -// .collectionViewBaseChrome-viewSpecsMenu-rowLeft, -// .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, -// .collectionViewBaseChrome-viewSpecsMenu-rowRight { -// font-size: $small-text; -// letter-spacing: 2px; -// color: $medium-gray; -// margin-left: 10px; -// padding: 5px; -// border: none; -// outline-color: $black; -// } -// } - -// .collectionViewBaseChrome-viewSpecsMenu-lastRow { -// display: grid; -// grid-template-columns: 1fr 1fr 1fr; -// grid-gap: 10px; -// margin: 10px; -// } -// } -// } -// } - -// .collectionStackingViewChrome-cont, -// .collectionTreeViewChrome-cont, -// .collection3DCarouselViewChrome-cont { -// display: flex; -// justify-content: space-between; -// } - -// .collectionGridViewChrome-cont { -// display: flex; -// margin-left: 10; - -// .collectionGridViewChrome-viewPicker { -// font-size: $small-text; -// //text-transform: uppercase; -// //letter-spacing: 2px; -// background: $dark-gray; -// color: $white; -// outline-color: $black; -// color: $white; -// border: none; -// border-right: solid $medium-gray 1px; -// } - -// .collectionGridViewChrome-viewPicker:active { -// outline-color: $black; -// } - -// .grid-control { -// align-self: center; -// display: flex; -// flex-direction: row; -// margin-right: 5px; - -// .grid-icon { -// margin-right: 5px; -// align-self: center; -// } - -// .flexLabel { -// margin-bottom: 0; -// } - -// .collectionGridViewChrome-entryBox { -// width: 50%; -// color: $black; -// } - -// .collectionGridViewChrome-columnButton { -// color: $black; -// } -// } -// } - -// .collectionStackingViewChrome-sort, -// .collectionTreeViewChrome-sort { -// display: flex; -// align-items: center; -// justify-content: space-between; - -// .collectionStackingViewChrome-sortIcon, -// .collectionTreeViewChrome-sortIcon { -// transition: transform .5s; -// margin-left: 10px; -// } -// } - -// button:hover { -// transform: scale(1); -// } - - -// .collectionStackingViewChrome-pivotField-cont, -// .collectionTreeViewChrome-pivotField-cont, -// .collection3DCarouselViewChrome-scrollSpeed-cont { -// justify-self: right; -// align-items: center; -// display: flex; -// grid-auto-columns: auto; -// font-size: $small-text; -// letter-spacing: 2px; - -// .collectionStackingViewChrome-pivotField-label, -// .collectionTreeViewChrome-pivotField-label, -// .collection3DCarouselViewChrome-scrollSpeed-label { -// grid-column: 1; -// margin-right: 7px; -// user-select: none; -// font-family: $sans-serif; -// letter-spacing: normal; -// } - -// .collectionStackingViewChrome-sortIcon { -// transition: transform .5s; -// grid-column: 3; -// text-align: center; -// display: flex; -// justify-content: center; -// align-items: center; -// cursor: pointer; -// width: 25px; -// height: 25px; -// border-radius: 100%; -// } - -// .collectionStackingViewChrome-sortIcon:hover { -// background-color: $drop-shadow; -// } - -// .collectionStackingViewChrome-pivotField, -// .collectionTreeViewChrome-pivotField, -// .collection3DCarouselViewChrome-scrollSpeed { -// color: $white; -// grid-column: 2; -// grid-row: 1; -// width: 90%; -// min-width: 100px; -// display: flex; -// height: 80%; -// border-radius: 7px; -// align-items: center; -// background: $white; - -// .editable-view-input, -// input, -// .editableView-container-editing-oneLine, -// .editableView-container-editing { -// margin: auto; -// border: 0px; -// color: $light-gray !important; -// text-align: center; -// letter-spacing: 2px; -// outline-color: $black; -// height: 100%; -// } - -// .react-autosuggest__container { -// margin: 0; -// color: $medium-gray; -// padding: 0px; -// } -// } -// } - -// .collectionStackingViewChrome-pivotField:hover, -// .collectionTreeViewChrome-pivotField:hover, -// .collection3DCarouselViewChrome-scrollSpeed:hover { -// cursor: text; -// } - -// } -// } - -// .collectionMenu-webUrlButtons { -// margin-left: 44; -// background: lightGray; -// display: flex; -// } - -// .webBox-urlEditor { -// position: relative; -// opacity: 0.9; -// z-index: 901; -// transition: top .5s; - -// .urlEditor { -// display: grid; -// grid-template-columns: 1fr auto; -// padding-bottom: 10px; -// overflow: hidden; -// margin-top: 5px; -// height: 35px; - -// .editorBase { -// display: flex; - -// .editor-collapse { -// transition: all .5s, opacity 0.3s; -// position: absolute; -// width: 40px; -// transform-origin: top left; -// } - -// .switchToText { -// color: $medium-gray; -// } - -// .switchToText:hover { -// color: $dark-gray; -// } -// } - -// button:hover { -// transform: scale(1); -// } -// } -// } - -// .collectionMenu-urlInput { -// padding: 12px 10px 11px 10px; -// border: 0px; -// color: $black; -// font-size: $small-text; -// letter-spacing: 2px; -// outline-color: $black; -// background: $white; -// width: 100%; -// min-width: 350px; -// margin-right: 10px; -// height: 100%; -// } - -// .collectionFreeFormMenu-cont { -// display: inline-flex; -// position: relative; -// align-items: center; -// height: 100%; - -// .color-previewI { -// width: 60%; -// top: 80%; -// position: absolute; -// height: 4px; -// } - -// .color-previewII { -// width: 80%; -// height: 80%; -// margin-left: 10%; -// position: absolute; -// bottom: 5; -// } - -// .btn-group { -// display: grid; -// grid-template-columns: auto auto auto auto; -// margin: auto; -// /* Make the buttons appear below each other */ -// } - -// .btn-draw { -// display: inline-flex; -// margin: auto; -// /* Make the buttons appear below each other */ -// } - -// .fwdKeyframe, -// .numKeyframe, -// .backKeyframe { -// cursor: pointer; -// position: relative; -// width: 20; -// height: 30; -// bottom: 0; -// background: $dark-gray; -// display: inline-flex; -// align-items: center; -// color: $white; -// } - -// .backKeyframe { -// svg { -// display: block; -// margin: auto; -// } -// } - - -// .numKeyframe { -// flex-direction: column; -// padding-top: 5px; -// } - -// .fwdKeyframe { -// svg { -// display: block; -// margin: auto; -// } - -// border-right: solid $medium-gray 1px; -// } -// } - -// .collectionSchemaViewChrome-cont { -// display: flex; -// font-size: $small-text; - -// .collectionSchemaViewChrome-toggle { -// display: flex; -// margin-left: 10px; -// } - -// .collectionSchemaViewChrome-label { -// text-transform: uppercase; -// letter-spacing: 2px; -// margin-right: 5px; -// display: flex; -// flex-direction: column; -// justify-content: center; -// } - -// .collectionSchemaViewChrome-toggler { -// width: 100px; -// height: 35px; -// background-color: $black; -// position: relative; -// } - -// .collectionSchemaViewChrome-togglerButton { -// width: 47px; -// height: 30px; -// background-color: $light-gray; -// // position: absolute; -// transition: all 0.5s ease; -// // top: 3px; -// margin-top: 3px; -// color: $medium-gray; -// letter-spacing: 2px; -// text-transform: uppercase; -// display: flex; -// flex-direction: column; -// justify-content: center; -// text-align: center; - -// &.on { -// margin-left: 3px; -// } - -// &.off { -// margin-left: 50px; -// } -// } -// } - - -// .commandEntry-outerDiv { -// display: flex; -// flex-direction: column; -// height: 40px; -// } - -// .commandEntry-inputArea { -// display: flex; -// flex-direction: row; -// width: 150px; -// margin: auto auto auto auto; -// } - -// .react-autosuggest__container { -// position: relative; -// width: 100%; -// margin-left: 5px; -// margin-right: 5px; -// } - -// .react-autosuggest__input { -// border: 1px solid $light-gray; -// border-radius: 4px; -// width: 100%; -// } - -// .react-autosuggest__input--focused { -// outline: none; -// } - -// .react-autosuggest__input--open { -// border-bottom-left-radius: 0; -// border-bottom-right-radius: 0; -// } - -// .react-autosuggest__suggestions-container { -// display: none; -// } - -// .react-autosuggest__suggestions-container--open { -// display: block; -// position: fixed; -// overflow-y: auto; -// max-height: 400px; -// width: 180px; -// border: 1px solid $light-gray; -// background-color: $white; -// font-family: $sans-serif; -// font-weight: 300; -// font-size: $large-header; -// border-bottom-left-radius: 4px; -// border-bottom-right-radius: 4px; -// z-index: 2; -// } - -// .react-autosuggest__suggestions-list { -// margin: 0; -// padding: 0; -// list-style-type: none; -// } - -// .react-autosuggest__suggestion { -// cursor: pointer; -// padding: 10px 20px; -// } - -// .react-autosuggest__suggestion--highlighted { -// background-color: $light-gray; -// }
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 615014c4a..f65e8698f 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -2,23 +2,23 @@ import React = require('react'); import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, Lambda, observable, reaction, runInAction } from 'mobx'; +import { Toggle, ToggleType, Type } from 'browndash-components'; +import { Lambda, action, computed, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState } from 'react-color'; +import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; -import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; -import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { Document } from '../../../fields/documentSchemas'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; @@ -28,20 +28,19 @@ import { undoBatch } from '../../util/UndoManager'; import { AntimodeMenu } from '../AntimodeMenu'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; -import { Colors } from '../global/globalEnums'; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; +import { MainView } from '../MainView'; +import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; -import { DocumentView, OpenWhereMod } from '../nodes/DocumentView'; -import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { DocumentView, DocumentViewInternal, OpenWhereMod } from '../nodes/DocumentView'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; -import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; -import { CollectionLinearView } from './collectionLinear'; import './CollectionMenu.scss'; import { COLLECTION_BORDER_WIDTH } from './CollectionView'; import { TabDocView } from './TabDocView'; import { CollectionFreeFormView } from './collectionFreeForm'; +import { CollectionLinearView } from './collectionLinear'; interface CollectionMenuProps { panelHeight: () => number; @@ -95,6 +94,15 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { } }; + @action + toggleProperties = () => { + if (MainView.Instance.propertiesWidth() > 0) { + SettingsManager.propertiesWidth = 0; + } else { + SettingsManager.propertiesWidth = 300; + } + }; + buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); @@ -121,7 +129,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { docViewPath={returnEmptyDoclist} moveDocument={returnFalse} addDocument={returnFalse} - addDocTab={returnFalse} + addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={emptyFunction} removeDocument={returnFalse} ScreenToLocalTransform={this.buttonBarXf} @@ -130,8 +138,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { renderDepth={0} focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -139,23 +147,45 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { } render() { - const propIcon = SettingsManager.headerBarHeight > 0 ? 'angle-double-up' : 'angle-double-down'; - const propTitle = SettingsManager.headerBarHeight > 0 ? 'Close Header Bar' : 'Open Header Bar'; - - const prop = ( - <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="topar" placement="bottom"> - <div className="collectionMenu-hardCodedButton" style={{ backgroundColor: SettingsManager.propertiesWidth > 0 ? Colors.MEDIUM_BLUE : undefined }} onPointerDown={this.toggleTopBar}> - <FontAwesomeIcon icon={propIcon} size="lg" /> - </div> - </Tooltip> + const headerIcon = SettingsManager.headerBarHeight > 0 ? 'angle-double-up' : 'angle-double-down'; + const headerTitle = SettingsManager.headerBarHeight > 0 ? 'Close Header Bar' : 'Open Header Bar'; + const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; + const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Properties' : 'Open Properties'; + + const hardCodedButtons = ( + <div className={`hardCodedButtons`}> + <Toggle + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + color={StrCast(Doc.UserDoc().userColor)} + onClick={this.toggleTopBar} + toggleStatus={SettingsManager.headerBarHeight > 0} + icon={<FontAwesomeIcon icon={headerIcon} size="lg" />} + tooltip={headerTitle} + /> + <Toggle + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + color={StrCast(Doc.UserDoc().userColor)} + onClick={this.toggleProperties} + toggleStatus={SettingsManager.propertiesWidth > 0} + icon={<FontAwesomeIcon icon={propIcon} size="lg" />} + tooltip={propTitle} + /> + </div> ); // NEW BUTTONS //dash col linear view buttons const contMenuButtons = ( - <div className="collectionMenu-container"> + <div + className="collectionMenu-container" + style={{ + background: SettingsManager.Instance.userBackgroundColor, + // borderColor: StrCast(Doc.UserDoc().userColor) + }}> {this.contMenuButtons} - {prop} + {hardCodedButtons} </div> ); @@ -172,7 +202,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { // [<CollectionViewBaseChrome key="chrome" // docView={this.SelectedCollection} // fieldKey={this.SelectedCollection.LayoutFieldKey} - // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + // type={StrCast(this.SelectedCollection?.props.Document._type_collection, CollectionViewType.Invalid) as CollectionViewType} />, // prop, // /*button*/]); } @@ -277,16 +307,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu _saveFilterCommand = { params: ['target'], title: 'save filter', - script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']); + script: `self.target._childFilters = compareLists(self['target-childFilters'],self.target._childFilters) ? undefined : copyField(self['target-childFilters']); self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, immediate: undoBatch((source: Doc[]) => { - this.target._docFilters = undefined; + this.target._childFilters = undefined; this.target._searchFilterDocs = undefined; }), initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { - button['target-docFilters'] = (Doc.MySearcher._docFilters || activeDash._docFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._docFilters || activeDash._docFilters) as any as ObjectField) : undefined; + button['target-childFilters'] = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as any as ObjectField) : undefined; button['target-searchFilterDocs'] = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs as any as ObjectField) : undefined; } }, @@ -347,9 +377,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch viewChanged = (e: React.ChangeEvent) => { - const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : (this.document.proto as Doc); + const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto); //@ts-ignore - target._viewType = e.target.selectedOptions[0].value; + target._type_collection = e.target.selectedOptions[0].value; }; commandChanged = (e: React.ChangeEvent) => { @@ -427,8 +457,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu if (docDragData?.draggedDocuments.length) { this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); + return true; } - return true; + return false; } dragViewDown = (e: React.PointerEvent) => { @@ -440,8 +471,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu const c = { params: ['target'], title: vtype, - script: `this.target._viewType = '${StrCast(this.props.type)}'`, - immediate: (source: Doc[]) => (this.document._viewType = Doc.getDocTemplate(source?.[0])), + script: `this.target._type_collection = '${StrCast(this.props.type)}'`, + immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])), initialize: emptyFunction, }; DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY); @@ -549,7 +580,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @action startRecording = () => { const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' }); - //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }; @@ -639,8 +669,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu key="float" style={{ backgroundColor: this.props.docView.layoutDoc.z ? '121212' : undefined, - pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'none' : undefined, - color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'dimgrey' : undefined, + pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'none' : undefined, + color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'dimgrey' : undefined, }} onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}> <FontAwesomeIcon icon={['fab', 'buffer']} size={'lg'} /> @@ -1005,14 +1035,12 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView if (Doc.noviceMode) { if (docs instanceof Doc) { - const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter( - key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') - ); + const noviceKeys = Array.from(keys).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } @@ -1124,13 +1152,13 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi if (Doc.UserDoc().noviceMode) { if (docs instanceof Doc) { - const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); const noviceKeys = Array.from(keys).filter( - key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') + key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') ); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index a65e23911..53a42d2a6 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -2,14 +2,15 @@ import React = require('react'); import { CursorProperty } from 'csstype'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Field, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -46,7 +47,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @observable _cursor: CursorProperty = 'grab'; @observable _scroll = 0; @computed get chromeHidden() { - return BoolCast(this.layoutDoc.chromeHidden); + return BoolCast(this.layoutDoc.chromeHidden) || this.props.onBrowseClick?.() ? true : false; } // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get colHeaderData() { @@ -191,7 +192,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { if (found) { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); - if (Math.floor(localTop[1]) !== 0) { + if (Math.floor(localTop[1]) !== 0 && Math.ceil(this.props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) { let focusSpeed = options.zoomTime ?? 500; smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); return focusSpeed; @@ -200,13 +201,17 @@ export class CollectionNoteTakingView extends CollectionSubView() { }; styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { - if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) { - return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`; - } - if (property === StyleProp.Opacity && doc) { - if (this.props.childOpacity) { - return this.props.childOpacity(); - } + switch (property) { + case StyleProp.BoxShadow: + if (doc && DragManager.docsBeingDragged.includes(doc)) { + return `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}`; + } + break; + case StyleProp.Opacity: + if (doc && this.props.childOpacity) { + return this.props.childOpacity(); + } + break; } return this.props.styleProvider?.(doc, props, property); }; @@ -225,7 +230,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { ref={r => (dref = r || undefined)} Document={doc} pointerEvents={this.blockPointerEventsWhenDragging} - DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} @@ -245,16 +250,17 @@ export class CollectionNoteTakingView extends CollectionSubView() { dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} layout_showTitle={this.props.childlayout_showTitle} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType} onClick={this.onChildClickHandler} + onBrowseClick={this.props.onBrowseClick} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={noteTakingDocTransform} focus={this.focusDocument} - docFilters={this.childDocFilters} + childFilters={this.childDocFilters} hideDecorationTitle={this.props.childHideDecorationTitle?.()} hideResizeHandles={this.props.childHideResizeHandles?.()} hideTitle={this.props.childHideTitle?.()} - docRangeFilters={this.childDocRangeFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} @@ -284,7 +290,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const existingHeader = this.colHeaderData.find(sh => sh.heading === heading); const existingWidth = existingHeader?.width ? existingHeader.width : 0; const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; - const width = d.layout_fitWidth ? maxWidth : d[WidthSym](); + const width = d.layout_fitWidth ? maxWidth : d[Width](); return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); } @@ -294,8 +300,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc; const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); - const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[WidthSym]() : 0); - const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[HeightSym]() : 0); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0); if (nw && nh) { const docWid = this.getDocWidth(d); return Math.min(maxHeight, (docWid * nh) / nw); @@ -436,23 +442,24 @@ export class CollectionNoteTakingView extends CollectionSubView() { docs.splice(previousDocIndex + 1, 0, ...newDocs); } } + return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); - this.props.addDocument?.(source); + if (!this.props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); - } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return true; + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { + return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + } return false; }; @undoBatch internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { const dropCreator = annoDragData.dropDocCreator; - annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { - const dropDoc = dropCreator(annotationOn); - return dropDoc || this.rootDoc; - }; + annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => dropCreator(annotationOn) || this.rootDoc; return true; } @@ -602,42 +609,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { return eles; } - @computed get buttonMenu() { - const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); - if (menuDoc) { - const width = NumCast(menuDoc._width, 30); - const height = NumCast(menuDoc._height, 30); - return ( - <div className="buttonMenu-docBtn" style={{ width, height }}> - <DocumentView - Document={menuDoc} - DataDoc={menuDoc} - isContentActive={this.props.isContentActive} - isDocumentActive={returnTrue} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - addDocTab={this.props.addDocTab} - pinToPres={emptyFunction} - rootSelected={this.props.isSelected} - removeDocument={this.props.removeDocument} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={() => 35} - PanelHeight={() => 35} - renderDepth={this.props.renderDepth} - focus={emptyFunction} - styleProvider={this.props.styleProvider} - docViewPath={returnEmptyDoclist} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - /> - </div> - ); - } - } - @computed get nativeWidth() { return Doc.NativeWidth(this.layoutDoc); } @@ -650,42 +621,32 @@ export class CollectionNoteTakingView extends CollectionSubView() { } @computed get backgroundEvents() { - return SnappingManager.GetIsDragging(); + return this.props.isContentActive() === false ? 'none' : undefined; } observer: any; render() { TraceMobx(); - const buttonMenu = this.rootDoc.buttonMenu; - const noviceExplainer = StrCast(this.rootDoc.explainer); return ( - <> - {buttonMenu || noviceExplainer ? ( - <div className="documentButtonMenu" key="buttons"> - {buttonMenu ? this.buttonMenu : null} - {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null} - </div> - ) : null} - <div - className="collectionNoteTakingView" - ref={this.createRef} - key="notes" - style={{ - overflowY: this.props.isContentActive() ? 'auto' : 'hidden', - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? 'all' : undefined, - }} - onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} - onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} - onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} - onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} - onDrop={this.onExternalDrop.bind(this)} - onContextMenu={this.onContextMenu} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> - {this.renderedSections} - </div> - </> + <div + className="collectionNoteTakingView" + ref={this.createRef} + key="notes" + style={{ + overflowY: this.props.isContentActive() ? 'auto' : 'hidden', + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? 'all' : undefined, + }} + onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} + onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} + onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} + onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> + {this.renderedSections} + </div> ); } } diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 63becac1e..3286d60bd 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -92,6 +92,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + return true; }); getValue = (value: string): any => { @@ -121,7 +122,8 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); @action pointerLeave = () => (this._background = 'inherit'); - textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + @undoBatch + addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true); // addNewTextDoc is called when a user starts typing in a column to create a new node @action @@ -272,7 +274,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu {!this.props.chromeHidden ? ( <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}> <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> - <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> </div> <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton"> <EditableView {...this.props.editableViewProps()} /> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index ea0fbbc54..bbd528e13 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; @@ -22,7 +23,7 @@ export class CollectionPileView extends CollectionSubView() { componentDidMount() { if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) { - this.Document._pileLayoutEngine = computePassLayout.name; + this.Document._freeform_pileEngine = computePassLayout.name; } this._originalChrome = this.layoutDoc._chromeHidden; this.layoutDoc._chromeHidden = true; @@ -32,7 +33,7 @@ export class CollectionPileView extends CollectionSubView() { Object.values(this._disposers).forEach(disposer => disposer?.()); } - layoutEngine = () => StrCast(this.Document._pileLayoutEngine); + layoutEngine = () => StrCast(this.Document._freeform_pileEngine); @undoBatch addPileDoc = (doc: Doc | Doc[]) => { @@ -51,20 +52,25 @@ export class CollectionPileView extends CollectionSubView() { @computed get toggleIcon() { return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' }); } + @computed get contentEvents() { + const isStarburst = this.layoutEngine() === computeStarburstLayout.name; + return this.props.isContentActive() && isStarburst ? undefined : 'none'; + } // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - const isStarburst = this.layoutEngine() === computeStarburstLayout.name; return ( - <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}> + <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}> <CollectionFreeFormView - {...this.props} - childContentsActive={returnFalse} + {...this.props} // layoutEngine={this.layoutEngine} addDocument={this.addPileDoc} - childCanEmbedOnDrag={true} - childClickScript={this.toggleIcon} moveDocument={this.removePileDoc} + // pile children never have their contents active, but will be document active whenever the entire pile is. + childContentsActive={returnFalse} + childDocumentsActive={this.props.isDocumentActive} + childDragAction="move" + childClickScript={this.toggleIcon} /> </div> ); @@ -72,28 +78,30 @@ export class CollectionPileView extends CollectionSubView() { // toggles the pileup between starburst to compact toggleStarburst = action(() => { + this.layoutDoc._freeform_scale = undefined; if (this.layoutEngine() === computeStarburstLayout.name) { - if (this.rootDoc[WidthSym]() !== NumCast(this.rootDoc._starburstDiameter, 500)) { - this.rootDoc._starburstDiameter = this.rootDoc[WidthSym](); + if (this.rootDoc[Width]() !== NumCast(this.rootDoc._starburstDiameter, 500)) { + this.rootDoc._starburstDiameter = this.rootDoc[Width](); } const defaultSize = 110; - this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; - this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; - this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize); - this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize); + this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - NumCast(this.layoutDoc._freeform_pileWidth, defaultSize) / 2; + this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - NumCast(this.layoutDoc._freeform_pileHeight, defaultSize) / 2; + this.layoutDoc._width = NumCast(this.layoutDoc._freeform_pileWidth, defaultSize); + this.layoutDoc._height = NumCast(this.layoutDoc._freeform_pileHeight, defaultSize); DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false); this.layoutDoc._freeform_panX = 0; this.layoutDoc._freeform_panY = -10; - this.props.Document._pileLayoutEngine = computePassLayout.name; + this.props.Document._freeform_pileEngine = computePassLayout.name; } else { - const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500); - this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2; - this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2; - this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym](); - this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym](); + const defaultSize = NumCast(this.rootDoc._starburstDiameter, 400); + this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - defaultSize / 2; + this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - defaultSize / 2; + this.layoutDoc._freeform_pileWidth = this.layoutDoc[Width](); + this.layoutDoc._freeform_pileHeight = this.layoutDoc[Height](); this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this.props.Document._pileLayoutEngine = computeStarburstLayout.name; + this.layoutDoc.background; + this.props.Document._freeform_pileEngine = computeStarburstLayout.name; } }); @@ -101,7 +109,6 @@ export class CollectionPileView extends CollectionSubView() { _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; - SnappingManager.SetIsDragging(true); setupMoveUpEvents( this, e, @@ -124,7 +131,6 @@ export class CollectionPileView extends CollectionSubView() { () => { this._undoBatch?.end(); this._undoBatch = undefined; - SnappingManager.SetIsDragging(false); }, emptyFunction, e.shiftKey && this.layoutEngine() === computePassLayout.name, diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index a55b70e22..a19d8e696 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -104,7 +104,7 @@ .collectionStackedTimeline-left-resizer, .collectionStackedTimeline-resizer { - background: $medium-gray; + background: $dark-gray; position: absolute; top: 0; height: 100%; diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 6b4c8a3e9..ad84d859d 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -369,22 +369,22 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack // handles dragging and dropping markers in timeline @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { - if (!de.embedKey && this.props.Document._isGroup) return false; - if (!super.onInternalDrop(e, de)) return false; - - // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view - const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - const x = localPt[0] - docDragData.offset[0]; - const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth); - docDragData.droppedDocuments.forEach(drop => { - const anchorEnd = this.anchorEnd(drop); - if (anchorEnd !== undefined) { - Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false); - } - Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false); - }); + if (super.onInternalDrop(e, de)) { + // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view + const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const x = localPt[0] - docDragData.offset[0]; + const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth); + docDragData.droppedDocuments.forEach(drop => { + const anchorEnd = this.anchorEnd(drop); + if (anchorEnd !== undefined) { + Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false); + } + Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false); + }); - return true; + return true; + } + return false; } onInternalDrop = (e: Event, de: DragManager.DropEvent) => { @@ -403,15 +403,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack docAnchor ?? Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any, - _minFontSize: 12, - _maxFontSize: 24, - _stayInCollection: true, + _label_minFontSize: 12, + _label_maxFontSize: 24, + _dragOnlyWithinContainer: true, backgroundColor: 'rgba(128, 128, 128, 0.5)', layout_hideLinkButton: true, onClick: FollowLinkScript(), annotationOn: rootDoc, - _timelineLabel: true, - borderRounding: anchorEndTime === undefined ? '100%' : undefined, + _isTimelineLabel: true, + layout_borderRounding: anchorEndTime === undefined ? '100%' : undefined, }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -527,6 +527,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack ); } + @computed get timelineEvents() { + return this.props.isContentActive() ? 'all' : this.props.isContentActive() === false ? 'none' : undefined; + } render() { const overlaps: { anchorStartTime: number; @@ -539,7 +542,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return this.clipDuration === 0 ? null : ( - <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> + <div ref={this.createDashEventsTarget} style={{ pointerEvents: this.timelineEvents }}> <div className="collectionStackedTimeline-timelineContainer" style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }} @@ -751,7 +754,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> if (timelineOnly) { if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined; Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - if (!left) Doc.SetInPlace(anchor, 'borderRounding', time !== undefined ? undefined : '100%', true); + if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', time !== undefined ? undefined : '100%', true); } else { anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time; } @@ -819,8 +822,8 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> focus={focusFunc} isContentActive={returnFalse} searchFilterDocs={returnEmptyDoclist} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} rootSelected={returnFalse} onClick={script} onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 99a68e94b..255bc3889 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -128,6 +128,7 @@ height: 15; position: absolute; margin-left: -5; + z-index: 10; } // Documents in stacking view diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 404ca5d65..e4a0d6dad 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,22 +1,23 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CursorProperty } from 'csstype'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; @@ -36,7 +37,7 @@ export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; chromeHidden?: boolean; // view type is stacking - viewType?: CollectionViewType; + type_collection?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; }; @@ -57,7 +58,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map<string, number>(); // Assuming that this is the current css cursor style - @observable _cursor: CursorProperty = 'grab'; + @observable _cursor: CursorProperty = 'ew-resize'; // gets reset whenever we scroll. Not sure what it is @observable _scroll = 0; // used to force the document decoration to update when scrolling // does this mean whether the browser is hidden? Or is chrome something else entirely? @@ -83,7 +84,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { - return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth())); + return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this.props.PanelWidth())); } @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth())); @@ -94,7 +95,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // are we stacking or masonry? @computed get isStackingView() { - return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; + return (this.props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking; } // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { @@ -285,7 +286,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const layout_fieldKey = StrCast(docView.LayoutFieldKey); const newDoc = Doc.MakeCopy(docView.rootDoc, true); const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) { newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey]; } @@ -294,7 +295,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); + isContentActive = () => (this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); @observable _renderCount = 5; isChildContentActive = () => @@ -306,21 +307,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ? false : undefined; isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined); + @observable docRefs = new ObservableMap<Doc, DocumentView>(); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number, count: number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); - let dref: Opt<DocumentView>; - const stackedDocTransform = () => this.getDocTransform(doc, dref); + const stackedDocTransform = () => this.getDocTransform(doc); this._docXfs.push({ stackedDocTransform, width, height }); - //DocumentView is how the node will be rendered return count > this._renderCount ? null : ( <DocumentView - ref={r => (dref = r || undefined)} + ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))} Document={doc} - DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} @@ -340,16 +340,16 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} layout_showTitle={this.props.childlayout_showTitle} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={stackedDocTransform} focus={this.focusDocument} - docFilters={this.childDocFilters} + childFilters={this.childDocFilters} hideDecorationTitle={this.props.childHideDecorationTitle?.()} hideResizeHandles={this.props.childHideResizeHandles?.()} hideTitle={this.props.childHideTitle?.()} - docRangeFilters={this.childDocRangeFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} xPadding={NumCast(this.layoutDoc._childXPadding, this.props.childXPadding)} yPadding={NumCast(this.layoutDoc._childYPadding, this.props.childYPadding)} @@ -366,9 +366,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); } - getDocTransform(doc: Doc, dref?: DocumentView) { - const y = this._scroll; // required for document decorations to update when the text box container is scrolled - const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); + getDocTransform(doc: Doc) { + const dref = this.docRefs.get(doc); + this._scroll; // must be referenced for document decorations to update when the text box container is scrolled + const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv); // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); } @@ -377,7 +378,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; if (!this.layoutDoc._columnsFill && !(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d))) { - return Math.min(d[WidthSym](), maxWidth); + return Math.min(d[Width](), maxWidth); } return maxWidth; } @@ -386,8 +387,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc; const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); - const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[WidthSym]() : 0); - const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[HeightSym]() : 0); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); @@ -401,11 +402,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { runInAction(() => (this._cursor = 'grabbing')); + const batch = UndoManager.StartBatch('stacking width'); setupMoveUpEvents( this, e, this.onDividerMove, - action(() => (this._cursor = 'grab')), + action(() => { + this._cursor = 'ew-resize'; + batch.end(); + }), emptyFunction ); }; @@ -417,7 +422,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection @computed get columnDragger() { return ( - <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}> + <div + 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` }}> <FontAwesomeIcon icon={'arrows-alt-h'} /> </div> ); @@ -454,18 +463,24 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const docs = this.childDocList; // still figuring out where to add the document if (docs && newDocs.length) { + newDocs.forEach(newdoc => docs.indexOf(newdoc) !== -1 && docs.splice(docs.indexOf(newdoc), 1)); const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0); newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); docs.splice(insertInd - offset, 0, ...newDocs); } + return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); - this.props.addDocument?.(source); + if (!this.props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); - } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return true; + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { + return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + } + e.preventDefault(); return false; }; @@ -583,7 +598,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection action((entries: any) => { if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0); - this.props.setHeight?.(this.headerMargin + height); + this.props.setHeight?.(2 * this.headerMargin + height); // bcz: added 2x for header to fix problem with scrollbars appearing in Tools panel } }) ); @@ -649,12 +664,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return35 = () => 35; @computed get buttonMenu() { - const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + const menuDoc: Doc = Cast(this.rootDoc.layout_headerButton, Doc, null); // TODO:glr Allow support for multiple buttons if (menuDoc) { const width: number = NumCast(menuDoc._width, 30); const height: number = NumCast(menuDoc._height, 30); - console.log(menuDoc.title, width, height); return ( <div className="buttonMenu-docBtn" style={{ width: width, height: height }}> <DocumentView @@ -678,8 +692,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} /> </div> @@ -699,7 +713,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } @computed get backgroundEvents() { - return SnappingManager.GetIsDragging(); + return this.props.isContentActive() === false ? 'none' : undefined; } observer: any; render() { @@ -709,8 +723,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection SetValue: this.addGroup, contents: '+ ADD A GROUP', }; - const buttonMenu = this.rootDoc.buttonMenu; - const noviceExplainer = this.rootDoc.explainer; + const buttonMenu = this.rootDoc.layout_headerButton; + const noviceExplainer = this.rootDoc.layout_explainer; return ( <> {buttonMenu || noviceExplainer ? ( @@ -726,7 +740,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection style={{ overflowY: this.isContentActive() ? 'auto' : 'hidden', background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: (this.props.pointerEvents?.() as any) ?? (this.backgroundEvents ? 'all' : undefined), + pointerEvents: (this.props.pointerEvents?.() as any) ?? this.backgroundEvents, }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} onDrop={this.onExternalDrop.bind(this)} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 243550c0b..ebb4ba5a1 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -94,6 +94,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; this.props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + return true; }); getValue = (value: string): any => { const parsed = parseInt(value); @@ -128,7 +129,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); @action pointerLeave = () => (this._background = 'inherit'); - textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { @@ -363,7 +364,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} - textCallback={this.textCallback} + textCallback={this.typedNote} placeholder={"Type ':' for commands"} contents={<FontAwesomeIcon icon={'plus'} />} menuCallback={this.menuCallback} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 008de1944..c189ef126 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,11 +1,11 @@ import { action, computed, observable } from 'mobx'; import * as rp from 'request-promise'; import CursorField from '../../../fields/CursorField'; -import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { AclPrivate } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { ScriptField } from '../../../fields/ScriptField'; import { Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -28,8 +28,8 @@ export function CollectionSubView<X>(moreProps?: X) { private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _mainCont?: HTMLDivElement; - @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it - @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it + @observable _focusFilters: Opt<string[]>; // childFilters that are overridden when previewing a link to an anchor which has childFilters set on it + @observable _focusRangeFilters: Opt<string[]>; // childFiltersByRanges that are overridden when previewing a link to an anchor which has childFiltersByRanges set on it protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view this.dropDisposer?.(); @@ -65,11 +65,7 @@ export function CollectionSubView<X>(moreProps?: X) { // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey]; - // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly - // setTimeout changes it outside of the @computed section - !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>())); - return this.dataDoc[this.props.fieldKey]; + return this.layoutDoc[this.props.fieldKey]; } get childLayoutPairs(): { layout: Doc; data: Doc }[] { @@ -85,13 +81,13 @@ export function CollectionSubView<X>(moreProps?: X) { get childDocList() { return Cast(this.dataField, listSpec(Doc)); } - collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters); - collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []); + collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._childFilters); + collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._childFiltersByRanges, listSpec('string'), []); // child filters apply to the descendants of the documents in this collection - childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; + childDocFilters = () => [...(this.props.childFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack - unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; - childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; + unrecursiveDocFilters = () => [...(this.props.childFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; + childDocRangeFilters = () => [...(this.props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); @@ -110,38 +106,41 @@ export function CollectionSubView<X>(moreProps?: X) { rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : []; } - const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc); + const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); const childDocFilters = this.childDocFilters(); - const docRangeFilters = this.childDocRangeFilters(); + const childFiltersByRanges = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); - if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.length && !searchDocs.length)) { + if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !childFiltersByRanges.length && !searchDocs.length)) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } const docsforFilter: Doc[] = []; childDocs.forEach(d => { // dragging facets - const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); + const dragged = this.props.childFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); if (dragged && DragManager.docsBeingDragged.includes(d)) return false; - let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, this.props.Document).length > 0; + let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.props.Document).length > 0; if (notFiltered) { - notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, this.props.Document).length > 0; + notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.props.Document).length > 0; const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - if (data !== undefined) { - let subDocs = DocListCast(data); + const side = annos && d[fieldKey + '_sidebar']; + if (data !== undefined || side !== undefined) { + let subDocs = [...DocListCast(data), ...DocListCast(side)]; if (subDocs.length > 0) { let newarray: Doc[] = []; - notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, d).length); + notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; subDocs.forEach(t => { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name); - notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, d).length)); + notFiltered = + notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -186,12 +185,14 @@ export function CollectionSubView<X>(moreProps?: X) { @undoBatch protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} - protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetAction: dropActionType) { + protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) { if (de.complete.docDragData) { - // if targetDropAction is, say 'embed', but we're just dragging within a collection, we want to ignore the targetAction. - // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys) - if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) { - de.complete.docDragData.dropAction = targetAction; + // override the dropEvent's dropAction + const dropAction = this.layoutDoc.dropAction as dropActionType; + // if the dropEvent's dragAction is, say 'embed', but we're just dragging within a collection, we may not actually want to make an embedding. + // so we check if our collection has a dropAction set on it and if so, we use that instead. + if (dropAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) { + de.complete.docDragData.dropAction = dropAction; } e.stopPropagation(); } @@ -204,7 +205,7 @@ export function CollectionSubView<X>(moreProps?: X) { protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; if (docDragData) { - let added = false; + let added = undefined; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); @@ -213,21 +214,21 @@ export function CollectionSubView<X>(moreProps?: X) { 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 = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); - added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); + const canAdd = de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc); + const moved = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse); + added = canAdd || moved ? moved : undefined; } else { - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData }); added = addedDocs.length ? this.addDocument(addedDocs) : true; } - added && e.stopPropagation(); - return added; } else { - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); + !added && alert('You cannot perform this move'); } - !added && alert('You cannot perform this move'); - e.stopPropagation(); - return added; + added === false && !this.props.isAnnotationOverlay && e.preventDefault(); + added === true && e.stopPropagation(); + return added ? true : false; } else if (de.complete.annoDragData) { const dropCreator = de.complete.annoDragData.dropDocCreator; de.complete.annoDragData.dropDocCreator = () => { @@ -448,7 +449,7 @@ export function CollectionSubView<X>(moreProps?: X) { } if (generatedDocuments.length) { // Creating a dash document - const isFreeformView = this.props.Document._viewType === CollectionViewType.Freeform; + const isFreeformView = this.props.Document._type_collection === CollectionViewType.Freeform; const set = !isFreeformView ? generatedDocuments : generatedDocuments.length > 1 diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 6135154c3..192d48c64 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -34,8 +34,6 @@ export class CollectionTimeView extends CollectionSubView() { async componentDidMount() { this.props.setContentView?.(this); - //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); - ///const childText = "const embedding = getEmbedding(self); switchView(embedding, detailView); embedding.dropAction='embed'; useRightSplit(embedding, shiftKey); "; runInAction(() => { this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name }); this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' }); @@ -47,11 +45,11 @@ export class CollectionTimeView extends CollectionSubView() { } getAnchor = (addAsAnnotation: boolean) => { - const anchor = Docs.Create.HTMLAnchorDocument([], { + const anchor = Docs.Create.HTMLMarkerDocument([], { title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, annotationOn: this.rootDoc, }); - PresBox.pinDocView(anchor, { pinData: { viewType: true, pivot: true, filters: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.rootDoc); if (addAsAnnotation) { // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered @@ -125,8 +123,8 @@ export class CollectionTimeView extends CollectionSubView() { goTo = (prevFilterIndex: number) => { this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex]; - this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); - this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); + this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); + this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); this.layoutDoc._prevFilterIndex = prevFilterIndex; }; @@ -136,7 +134,7 @@ export class CollectionTimeView extends CollectionSubView() { if (prevFilterIndex > 0) { this.goTo(prevFilterIndex - 1); } else { - this.layoutDoc._docFilters = new List([]); + this.layoutDoc._childFilters = new List([]); } }; @@ -145,7 +143,7 @@ export class CollectionTimeView extends CollectionSubView() { <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}> <CollectionFreeFormView {...this.props} - engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }} + engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }} fitContentsToBox={returnTrue} childClickScript={this._childClickedScript} viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick} @@ -276,12 +274,12 @@ export class CollectionTimeView extends CollectionSubView() { ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { const pivotField = StrCast(pivotDoc._pivotField, 'author'); let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); - const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField)); - pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); - pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField)); + pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField); + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField); pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField; pivotDoc._prevFilterIndex = ++prevFilterIndex; - pivotDoc._docFilters = new List(); + pivotDoc._childFilters = new List(); setTimeout( action(() => { const filterVals = bounds.payload as string[]; @@ -292,7 +290,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou pivotDoc._pivotField = filterVals[0]; } } - const newFilters = StrListCast(pivotDoc._docFilters); + const newFilters = StrListCast(pivotDoc._childFilters); if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) { pivotDoc._prevFilterIndex = --prevFilterIndex; pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 273b08247..2bf649caf 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -13,7 +13,7 @@ width: 100%; position: relative; top: 0; - background: $light-gray; + // background: $light-gray; font-size: 13px; overflow: auto; user-select: none; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 095e34c39..ea3b5065f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,8 +1,8 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; -import { InkTool } from '../../../fields/InkField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -89,8 +89,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); - isContentActive = (outsideReaction?: boolean) => - Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction) ? true : false; + isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this.props.isContentActive() ? true : false); componentWillUnmount() { this._isDisposing = true; @@ -141,11 +140,17 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); }; - protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent) => { + const dropAction = this.layoutDoc.dropAction as dropActionType; const dragData = de.complete.docDragData; if (dragData) { - const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); - dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? 'same' : dragData.dropAction; + const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.rootDoc) ? true : false; + const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); + if (isAlreadyInTree() !== sameTree) { + console.log('WHAAAT'); + } + dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree ? 'same' : dragData.dropAction; + e.stopPropagation(); } }; @@ -154,15 +159,15 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @action remove = (doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; - const targetDataDoc = this.doc[DataSym]; + const targetDataDoc = this.doc[DocData]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views().some(dv => Doc.AreProtosEqual(dv.rootDoc, doc)))) SelectionManager.DeselectAll(); - if (result.length !== value.length) { - const ind = targetDataDoc[this.props.fieldKey].indexOf(doc); - const prev = ind && targetDataDoc[this.props.fieldKey][ind - 1]; + if (result.length !== value.length && doc instanceof Doc) { + const ind = DocListCast(targetDataDoc[this.props.fieldKey]).indexOf(doc); + const prev = ind && DocListCast(targetDataDoc[this.props.fieldKey])[ind - 1]; this.props.removeDocument?.(doc); - if (ind > 0) { + if (ind > 0 && prev) { FormattedTextBox.SelectOnLoad = prev[Id]; DocumentManager.Instance.getDocumentView(prev, this.props.DocumentView?.())?.select(false); } @@ -175,8 +180,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => { - const res = flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before); - res && (doc.embedContainer = this.props.Document); + const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before); + res && Doc.SetContainer(doc, this.props.Document); return res; }, true); if (this.doc.resolvedDataDoc instanceof Promise) return false; @@ -239,8 +244,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree PanelHeight={this.documentTitleHeight} NativeDimScaling={returnOne} onKey={this.onKey} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={returnFalse} moveDocument={returnFalse} @@ -260,7 +265,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _renderCount = 1; @computed get treeViewElements() { TraceMobx(); - const dropAction = StrCast(this.doc.childDropAction) as dropActionType; + const dragAction = StrCast(this.doc.childDragAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false; if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20)))); @@ -275,7 +280,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree addDoc, this.remove, moveDoc, - dropAction, + dragAction, this.props.addDocTab, this.props.styleProvider, this.screenToLocalTransform, @@ -313,12 +318,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } @computed get noviceExplainer() { - return !Doc.noviceMode || !this.rootDoc.explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.explainer)} </div>; + return !Doc.noviceMode || !this.rootDoc.layout_explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.layout_explainer)} </div>; } return35 = () => 35; @computed get buttonMenu() { - const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + const menuDoc = Cast(this.rootDoc.layout_headerButton, Doc, null); // To create a multibutton menu add a CollectionLinearView return !menuDoc ? null : ( <div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}> @@ -342,8 +347,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} /> </div> @@ -368,8 +373,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree marginX = () => NumCast(this.doc._xMargin); marginTop = () => NumCast(this.doc._yMargin); marginBot = () => NumCast(this.doc._yMargin); - documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); - documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins); + documentTitleWidth = () => Math.min(this.layoutDoc?.[Width](), this.panelWidth()); + documentTitleHeight = () => (this.layoutDoc?.[Height]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 1)); @@ -382,7 +387,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _headerHeight = 0; @computed get content() { const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); - const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined); + const color = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.Color); + const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined); const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar; return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}> @@ -395,8 +401,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree <div className="collectionTreeView-contents" key="tree" + ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)} style={{ ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}), + color: color(), overflow: 'auto', width: '100%', height: '100%', @@ -418,8 +426,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree minHeight: '100%', }} onWheel={e => e.stopPropagation()} - onDrop={this.onTreeDrop} - ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}> + onDrop={this.onTreeDrop}> <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul> </div> </div> @@ -439,12 +446,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} + pointerEvents={this.props.isContentActive() && SnappingManager.GetIsDragging() ? returnAll : returnNone} isAnnotationOverlay={true} isAnnotationOverlayScrollable={true} childDocumentsActive={this.props.isDocumentActive} fieldKey={this.props.fieldKey + '_annotations'} - dropAction={'move'} + dropAction="move" select={emptyFunction} addDocument={this.addAnnotationDocument} removeDocument={this.remAnnotationDocument} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index dd7bf1740..c33519afb 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -9,6 +9,7 @@ import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; +import { dropActionType } from '../../util/DragManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; @@ -55,7 +56,7 @@ interface CollectionViewProps_ extends FieldViewProps { childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection - childCanEmbedOnDrag?: boolean; + childDragAction?: dropActionType; childXPadding?: number; childYPadding?: number; childLayoutString?: string; @@ -87,7 +88,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } get collectionViewType(): CollectionViewType | undefined { - const viewField = StrCast(this.layoutDoc._viewType); + const viewField = StrCast(this.layoutDoc._type_collection); if (CollectionView._safeMode) { switch (viewField) { case CollectionViewType.Freeform: @@ -127,8 +128,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } }; - setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc) { - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { + setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { // prettier-ignore const subItems: ContextMenuProps[] = [ { description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' }, @@ -160,7 +161,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab !Doc.noviceMode && this.setupViewTypes('UI Controls...', vtype => { const newRendition = Doc.MakeEmbedding(this.rootDoc); - newRendition._viewType = vtype; + newRendition._type_collection = vtype; this.props.addDocTab(newRendition, OpenWhere.addRight); return newRendition; }); @@ -220,7 +221,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles); childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle); childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); - isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() || this.isAnyChildContentActive(); + isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive()); render() { TraceMobx(); @@ -244,7 +245,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab <div className="collectionView" onContextMenu={this.onContextMenu} - style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}> + style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}> {this.renderSubView(this.collectionViewType, props)} </div> ); diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index 58605c3f4..13bb3a577 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -65,11 +65,7 @@ input.lm_title { } .miniMap { - position: absolute; overflow: hidden; - right: 15; - bottom: 15; - border: solid 1px; width: 100%; height: 100%; transition: all 0.5s; @@ -90,18 +86,5 @@ input.lm_title { cursor: pointer; position: absolute; bottom: 5; - display: flex; right: 5; - width: 25px; - height: 25px; - border-radius: 3px; - padding: 2px; - justify-content: center; - align-items: center; - align-content: center; - background-color: $light-gray; - - &:hover { - box-shadow: none; - } } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 69963736b..d787f5262 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -2,25 +2,24 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { clamp } from 'lodash'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; -import { listSpec } from '../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; -import { DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; import { Colors, Shadows } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; @@ -35,6 +34,7 @@ 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 { @@ -44,12 +44,21 @@ interface TabDocViewProps { } @observer export class TabDocView extends React.Component<TabDocViewProps> { + static _allTabs = new ObservableSet<TabDocView>(); _mainCont: HTMLDivElement | null = null; _tabReaction: IReactionDisposer | undefined; @observable _activated: boolean = false; @observable _panelWidth = 0; @observable _panelHeight = 0; + @observable _hovering = false; @observable _isActive: boolean = false; + @observable _isAnyChildContentActive = false; + @computed get _isUserActivated() { + return SelectionManager.Views().some(view => view.rootDoc === this._document) || this._isAnyChildContentActive; + } + @computed get _isContentActive() { + return this._isUserActivated || this._hovering; + } @observable _document: Doc | undefined; @observable _view: DocumentView | undefined; @@ -62,9 +71,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { return 'transparent'; } @computed get tabColor() { - let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); - if (tabColor === 'transparent') return 'black'; - return tabColor; + return this._isUserActivated ? Colors.WHITE : this._hovering ? Colors.LIGHT_GRAY : Colors.MEDIUM_GRAY; } @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? 'black' : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); @@ -116,15 +123,13 @@ export class TabDocView extends React.Component<TabDocViewProps> { titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; - titleEle.onkeydown = (e: KeyboardEvent) => { - e.stopPropagation(); - }; - titleEle.onchange = undoBatch( - action((e: any) => { + titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation(); + titleEle.onchange = (e: any) => { + undoable(() => { titleEle.size = e.currentTarget.value.length + 3; Doc.GetProto(doc).title = e.currentTarget.value; - }) - ); + }, 'edit tab title')(); + }; if (tab.element[0].children[1].children.length === 1) { iconWrap.className = 'lm_iconWrap lm_moreInfo'; @@ -194,11 +199,13 @@ export class TabDocView extends React.Component<TabDocViewProps> { } }); tab._disposers.selectionDisposer = reaction( - () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc), + () => SelectionManager.Views().some(view => view.rootDoc === this._document), action(selected => { if (selected) this._activated = true; const toggle = tab.element[0].children[2].children[0] as HTMLInputElement; - selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch'); + if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')(); + } toggle.style.fontWeight = selected ? 'bold' : ''; // toggle.style.textTransform = selected ? "uppercase" : ""; }), @@ -234,7 +241,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; - const batch = UndoManager.StartBatch('pinning doc'); + const batch = UndoManager.StartBatch('Pin doc to pres trail'); const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); if (!Doc.ActivePresentation) { @@ -256,7 +263,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom; pinDoc.presDuration = pinDoc.presDuration ?? 1000; pinDoc.groupWithUp = false; - pinDoc.embedContainer = curPres; + Doc.SetContainer(pinDoc, curPres); // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling. @@ -294,18 +301,19 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array }); if ( + // open the presentation trail if it's not already opened !Array.from(CollectionDockingView.Instance?.tabMap ?? []) .map(d => d.DashDoc) .includes(curPres) ) { - const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); - if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); + if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres); CollectionDockingView.AddSplit(curPres, OpenWhereMod.right); setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things } setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } + @action componentDidMount() { new _global.ResizeObserver( action((entries: any) => { @@ -320,14 +328,17 @@ export class TabDocView extends React.Component<TabDocViewProps> { // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), // { fireImmediately: true }); + TabDocView._allTabs.add(this); } componentDidUpdate() { this._view && DocumentManager.Instance.AddView(this._view); } + @action componentWillUnmount() { this._tabReaction?.(); this._view && DocumentManager.Instance.RemoveView(this._view); + TabDocView._allTabs.delete(this); this.props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged); } @@ -336,6 +347,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)); !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. } } @@ -349,10 +361,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { // lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox addDocTab = (doc: Doc, location: OpenWhere) => { SelectionManager.DeselectAll(); - const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereFields = location.split(':'); const keyValue = whereFields[1]?.includes('KeyValue'); const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none; - if (doc.dockingConfig) return DashboardView.openDashboard(doc); + if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc); // prettier-ignore switch (whereFields[0]) { case undefined: @@ -365,8 +377,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { } } return LightboxView.AddDocTab(doc, location); - case OpenWhere.dashboard: return DashboardView.openDashboard(doc); - case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); 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); @@ -383,10 +393,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { }; getCurrentFrame = () => { - return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); + return NumCast(Cast(PresBox.Instance.activeItem.presentationTargetDoc, Doc, null)._currentFrame); }; static Activate = (tabDoc: Doc) => { - const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc); + const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue); tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) return tab !== undefined; }; @@ -406,11 +416,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { }; PanelWidth = () => this._panelWidth; PanelHeight = () => this._panelHeight; - miniMapColor = () => this.tabColor; + miniMapColor = () => Colors.MEDIUM_GRAY; tabView = () => this._view; - disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform; - hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap); - + disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform; + whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive); + isContentActive = () => this._isContentActive; @computed get docView() { return !this._activated || !this._document ? null : ( <> @@ -425,16 +435,16 @@ export class TabDocView extends React.Component<TabDocViewProps> { LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString() : undefined} hideTitle={this.props.keyValue} Document={this._document} - DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} onBrowseClick={MainView.Instance.exploreMode} waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick} - isContentActive={returnTrue} + isContentActive={this.isContentActive} isDocumentActive={returnFalse} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} styleProvider={DefaultStyleProvider} - docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} addDocument={undefined} removeDocument={this.remDocTab} @@ -442,30 +452,15 @@ export class TabDocView extends React.Component<TabDocViewProps> { ScreenToLocalTransform={this.ScreenToLocalTransform} dontCenter={'y'} rootSelected={returnTrue} - whenChildContentsActiveChanged={emptyFunction} + whenChildContentsActiveChanged={this.whenChildContentActiveChanges} focus={this.focusFunc} docViewPath={returnEmptyDoclist} bringToFront={emptyFunction} pinToPres={TabDocView.PinDoc} /> - <TabMinimapView key="minimap" hideMinimap={this.hideMinimap} addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.layout_hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}> - <div - className="miniMap-hidden" - style={{ - display: this.disableMinimap() || this._document._viewType !== 'freeform' ? 'none' : undefined, - color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE, - backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, - boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined, - }} - onPointerDown={e => e.stopPropagation()} - onClick={action(e => { - e.stopPropagation(); - this._document!.layout_hideMinimap = !this._document!.layout_hideMinimap; - })}> - <FontAwesomeIcon icon={'globe-asia'} size="lg" /> - </div> - </Tooltip> + {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} /> + )} </> ); } @@ -477,6 +472,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { style={{ fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined, }} + onPointerEnter={action(() => (this._hovering = true))} + onPointerLeave={action(() => (this._hovering = false))} + onDragOver={action(() => (this._hovering = true))} + onDragLeave={action(() => (this._hovering = false))} ref={ref => { if ((this._mainCont = ref)) { if (this._lastTab) { @@ -496,7 +495,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { interface TabMinimapViewProps { document: Doc; - hideMinimap: () => boolean; tabView: () => DocumentView | undefined; addDocTab: (doc: Doc, where: OpenWhere) => boolean; PanelWidth: () => number; @@ -535,7 +533,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { return 'gray'; } })(doc.type as DocumentType); - return !background ? undefined : <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: 'absolute', display: 'block', background }} />; + return !background ? undefined : <div style={{ width: doc[Width](), height: doc[Height](), position: 'absolute', display: 'block', background }} />; } } }; @@ -571,46 +569,56 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { 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_) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; + 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; const miniSize = this.returnMiniSize(); - return this.props.hideMinimap() ? null : ( - <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} - dropAction={undefined} - 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} - docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} - searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} - fitContentsToBox={returnTrue} + 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="miniOverlay" onPointerDown={this.miniDown}> - <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} /> - </div> </div> ); } diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 7eab03e1d..d22e85880 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -13,7 +13,7 @@ .treeView-container-active { .bullet-outline { position: relative; - width: $TREE_BULLET_WIDTH; + width: fit-content; color: $medium-gray; transform: scale(0.5); display: inline-flex; @@ -21,37 +21,20 @@ } .treeView-bulletIcons { - // width: $TREE_BULLET_WIDTH; width: 100%; height: 100%; - position: absolute; - - .treeView-expandIcon { - display: none; - left: -10px; - position: absolute; - } - - .treeView-checkIcon { - left: 3.5px; - top: 2px; - position: absolute; - } - - &:hover { - .treeView-expandIcon { - display: unset; - } - } + position: relative; + display: flex; + flex-direction: row; } - .treeView-bulletIcons:hover img { - left: 14px; - position: absolute; - transform-origin: center left; - transform: scale(6); - pointer-events: none; - } + // .treeView-bulletIcons:hover img { + // left: 14px; + // position: absolute; + // transform-origin: center left; + // transform: scale(6); + // pointer-events: none; + // } .bullet { grid-column: 1; @@ -59,11 +42,12 @@ justify-content: center; align-items: center; position: relative; - width: $TREE_BULLET_WIDTH; + width: fit-content; min-height: 20px; color: $medium-gray; border: #80808030 1px solid; border-radius: 5px; + z-index: 1; } } @@ -71,9 +55,6 @@ position: absolute; height: max-content; pointer-events: none; - color: white; - border-radius: 4px; - font-size: 10px; } .treeView-container-active { @@ -121,10 +102,26 @@ align-items: center; width: max-content; border-radius: 5px; + overflow: hidden; + position: relative; + z-index: 1; + + .treeView-background { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 0; + filter: opacity(0); + } &:hover { - background-color: #bdddf5; + .treeView-background { + filter: opacity(0.2) !important; + } } + //align-items: center; ::-webkit-scrollbar { @@ -157,6 +154,7 @@ opacity: 0.75; pointer-events: all; cursor: pointer; + z-index: 1; > svg { margin-left: 0.25rem; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 92b70cb5a..25a547066 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -2,7 +2,8 @@ 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 { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -10,7 +11,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -20,7 +21,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss'; import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView'; @@ -33,6 +34,7 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView'; import { CollectionView } from './CollectionView'; import './TreeView.scss'; import React = require('react'); +import { IconButton, Size } from 'browndash-components'; export interface TreeViewProps { treeView: CollectionTreeView; @@ -44,7 +46,7 @@ export interface TreeViewProps { dataDoc?: Doc; treeViewParent: Doc; renderDepth: number; - dropAction: dropActionType; + dragAction: dropActionType; addDocTab: (doc: Doc, where: OpenWhere) => boolean; panelWidth: () => number; panelHeight: () => number; @@ -102,7 +104,7 @@ export class TreeView extends React.Component<TreeViewProps> { private _treedropDisposer?: DragManager.DragDropDisposer; get treeViewOpenIsTransient() { - return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDocDataProto(this.doc); + return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc); } set treeViewOpen(c: boolean) { if (this.treeViewOpenIsTransient) this._transientOpenState = c; @@ -118,14 +120,14 @@ export class TreeView extends React.Component<TreeViewProps> { return 'TreeView(' + this.props.document.title + ')'; } // this makes mobx trace() statements more descriptive get defaultExpandedView() { - return this.doc.viewType === CollectionViewType.Docking + return this.doc._type_collection === CollectionViewType.Docking ? this.fieldKey : this.props.treeView.dashboardMode ? this.fieldKey : this.props.treeView.fileSysMode ? this.doc.isFolder ? this.fieldKey - : 'embeddings' // for displaying + : 'data' // file system folders display their contents (data). used to be they displayed their embeddings but now its a tree structure and not a flat list : this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.noviceMode @@ -146,7 +148,7 @@ export class TreeView extends React.Component<TreeViewProps> { return NumCast(this.props.treeViewParent.maxEmbedHeight, 200); } @computed get dataDoc() { - return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DataSym]; + return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DocData]; } @computed get layoutDoc() { return Doc.Layout(this.doc); @@ -191,10 +193,10 @@ export class TreeView extends React.Component<TreeViewProps> { }; @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { this.props.treeView.props.select(false); - const ind = this.dataDoc[key].indexOf(doc); + const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement()); const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); - res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.DocumentView?.())?.select(false); + res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.props.treeView.props.DocumentView?.())?.select(false); return res; }; @@ -220,12 +222,8 @@ export class TreeView extends React.Component<TreeViewProps> { this.treeViewOpen = !this.treeViewOpen; } else { // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding - const bestEmbedding = - docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDocDataProto(docView.props.Document) - ? docView.props.Document - : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); - const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail); - this.props.addDocTab(bestEmbedding ?? nextBestEmbedding ?? Doc.MakeEmbedding(this.props.document), OpenWhere.lightbox); + const bestEmbedding = docView.rootDoc.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) ? docView.rootDoc : Doc.BestEmbedding(docView.rootDoc); + this.props.addDocTab(bestEmbedding, OpenWhere.lightbox); } }; @@ -275,7 +273,7 @@ export class TreeView extends React.Component<TreeViewProps> { }; onPointerEnter = (e: React.PointerEvent): void => { this.props.isContentActive(true) && Doc.BrushDoc(this.dataDoc); - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + if (e.buttons === 1 && SnappingManager.GetIsDragging() && this.props.isContentActive()) { this._header.current!.className = 'treeView-header'; document.removeEventListener('pointermove', this.onDragMove, true); document.removeEventListener('pointerup', this.onDragUp, true); @@ -312,7 +310,7 @@ export class TreeView extends React.Component<TreeViewProps> { title: '-title-', treeViewExpandedViewLock: true, treeViewExpandedView: 'data', - _viewType: CollectionViewType.Tree, + _type_collection: CollectionViewType.Tree, layout_hideLinkButton: true, _layout_showSidebar: true, _layout_fitWidth: true, @@ -340,13 +338,12 @@ export class TreeView extends React.Component<TreeViewProps> { }; makeFolder = () => { - const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }); TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView }; return this.props.addDocument(folder); }; - deleteItem = () => this.props.removeDoc?.(this.doc); - preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + preTreeDrop = (e: Event, de: DragManager.DropEvent) => { const dragData = de.complete.docDragData; dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? 'same' : dragData.dropAction); }; @@ -354,7 +351,7 @@ export class TreeView extends React.Component<TreeViewProps> { @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { const pt = [de.x, de.y]; - if (!this._header.current) return; + 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); @@ -363,14 +360,25 @@ export class TreeView extends React.Component<TreeViewProps> { const destDoc = this.doc; DocUtils.MakeLink(sourceDoc, destDoc, { link_relationship: 'tree link' }); e.stopPropagation(); + return true; } const docDragData = de.complete.docDragData; if (docDragData && pt[0] < rect.left + rect.width) { if (docDragData.draggedDocuments[0] === this.doc) return true; - if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.removeDocument, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { - e.stopPropagation(); - } + const added = this.dropDocuments( + docDragData.droppedDocuments, // + before, + inside, + docDragData.dropAction, + docDragData.removeDocument, + docDragData.moveDocument, + docDragData.treeViewDoc === this.props.treeView.props.Document + ); + e.stopPropagation(); + !added && e.preventDefault(); + return added; } + return false; }; dropping: boolean = false; @@ -380,17 +388,17 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && (doc.embedContainer = this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; 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 canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.freezeChildren).includes('add')) || forceAdd; + const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeViewFreezeChildren).includes('add')) || forceAdd; if (canAdd) { this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true); - const res = UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false)); + 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); return res; } @@ -410,7 +418,7 @@ export class TreeView extends React.Component<TreeViewProps> { embeddedPanelHeight = () => { const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; return Math.min( - layoutDoc[HeightSym](), + layoutDoc[Height](), this.MAX_EMBED_HEIGHT, (() => { const aspect = Doc.NativeAspect(layoutDoc); @@ -419,7 +427,7 @@ export class TreeView extends React.Component<TreeViewProps> { ? !Doc.NativeHeight(layoutDoc) ? NumCast(layoutDoc._height) : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.treeViewParent._height))) - : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym](); + : (this.embeddedPanelWidth() * layoutDoc[Height]()) / layoutDoc[Width](); })() ); }; @@ -443,7 +451,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - dataIsComputed && (doc.embedContainer = this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -459,7 +467,7 @@ export class TreeView extends React.Component<TreeViewProps> { addDoc, remDoc, moveDoc, - this.props.dropAction, + this.props.dragAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, @@ -528,7 +536,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderContent() { TraceMobx(); const expandKey = this.treeViewExpandedView; - const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } }) ?? {}; + const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; icon: JSX.Element | string } }) ?? {}; if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) { const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None); const sortKeys = Object.keys(sortings); @@ -551,7 +559,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.embedContainer = this.doc.embedContainer); + !dataIsComputed && added && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; @@ -568,10 +576,25 @@ 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'} style={{ background: sortings[sorting]?.color }}> - {sortings[sorting]?.label} + <div className={'treeView-sorting'}> + <IconButton + color={sortings[sorting]?.color} + size={Size.XSMALL} + icon={sortings[sorting]?.icon} + onPointerDown={e => { + downX = e.clientX; + downY = e.clientY; + e.stopPropagation(); + }} + onClick={undoable(e => { + if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) { + !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]); + e.stopPropagation(); + } + }, 'sort order')} + /> </div> )} <ul @@ -584,12 +607,12 @@ export class TreeView extends React.Component<TreeViewProps> { downY = e.clientY; e.stopPropagation(); }} - onClick={e => { + onClick={undoable(e => { if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) { !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]); e.stopPropagation(); } - }}> + }, 'sort order')}> {!docs ? null : TreeView.GetChildElements( @@ -603,7 +626,7 @@ export class TreeView extends React.Component<TreeViewProps> { addDoc, remDoc, moveDoc, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, + StrCast(this.doc.childDragAction, this.props.dragAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, @@ -628,7 +651,7 @@ export class TreeView extends React.Component<TreeViewProps> { this._renderCount )} </ul> - </> + </div> ); } else if (this.treeViewExpandedView === 'fields') { return ( @@ -639,6 +662,7 @@ export class TreeView extends React.Component<TreeViewProps> { } return ( <ul + style={{}} onPointerDown={e => { e.preventDefault(); e.stopPropagation(); @@ -672,7 +696,8 @@ 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' : '')) || 'question'; + 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 checked = this.onCheckedClick ? this.doc.treeViewChecked ?? 'unchecked' : undefined; return ( <div @@ -693,14 +718,19 @@ export class TreeView extends React.Component<TreeViewProps> { }> {this.props.treeView.outlineMode ? ( !(this.doc.text as RichTextField)?.Text ? null : ( - <FontAwesomeIcon size="sm" icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} /> + <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 }}> - <div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}> - <FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} /> - </div> - {this.onCheckedClick ? null : typeof iconType === 'string' ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType} + {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} + /> + ) : ( + <IconButton color={color} icon={<FontAwesomeIcon icon={iconType as IconProp} />} size={Size.XSMALL} /> + )} </div> )} </div> @@ -713,7 +743,7 @@ export class TreeView extends React.Component<TreeViewProps> { const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : ''); const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings'); const fields = () => (Doc.noviceMode ? '' : 'fields'); - const layout = Doc.noviceMode || this.doc.viewType === CollectionViewType.Docking ? [] : ['layout']; + const layout = Doc.noviceMode || this.doc._type_collection === CollectionViewType.Docking ? [] : ['layout']; return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [embeddings(), links(), annos()] : []), fields()].filter(m => m); } @action @@ -728,21 +758,19 @@ 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); return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : ( <> {customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */} - {this.doc.hideContextMenu ? null : ( - <FontAwesomeIcon - title="context menu" - key="bars" - icon="bars" - size="sm" - onClick={e => { - this.showContextMenu(e); - e.stopPropagation(); - }} - /> - )} + <IconButton + color={color} + icon={<FontAwesomeIcon icon="bars" />} + size={Size.XSMALL} + onClick={e => { + this.showContextMenu(e); + e.stopPropagation(); + }} + /> {Doc.noviceMode ? null : this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : ( <span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}> {this.treeViewExpandedView} @@ -759,10 +787,10 @@ export class TreeView extends React.Component<TreeViewProps> { }; contextMenuItems = () => { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' }; - const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' }; const folderOp = this.childDocs?.length ? [makeFolder] : []; - const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Embedding' }; + const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' }; + const reopenDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Reopen' }; return [ ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), ...(this.doc.isFolder @@ -771,9 +799,11 @@ export class TreeView extends React.Component<TreeViewProps> { ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? [openEmbedding, makeFolder] - : this.doc.viewType === CollectionViewType.Docking + : this.doc._type_collection === CollectionViewType.Docking ? [] - : [deleteItem, openEmbedding, focusDoc]), + : this.props.treeView.rootDoc === Doc.MyRecentlyClosed + ? [reopenDoc] + : [openEmbedding, focusDoc]), ]; }; childContextMenuItems = () => { @@ -856,7 +886,6 @@ export class TreeView extends React.Component<TreeViewProps> { }; titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - this.headerEleWidth - treeBulletWidth(); - return18 = () => 18; /** * Renders the EditableView title element for placement into the tree. */ @@ -874,6 +903,7 @@ export class TreeView extends React.Component<TreeViewProps> { height={12} sizeToContent={true} fontSize={12} + isEditingCallback={action(e => (this._editTitle = e))} GetValue={() => StrCast(this.doc.title)} OnTab={undoBatch((shift?: boolean) => { if (!shift) this.props.indentDocument?.(true); @@ -904,7 +934,6 @@ export class TreeView extends React.Component<TreeViewProps> { hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} styleProvider={this.titleStyleProvider} - enableDragWhenActive={true} onClickScriptDisable="never" // tree docViews have a script to show fields, etc. docViewPath={this.props.treeView.props.docViewPath} treeViewDoc={this.props.treeView.props.Document} @@ -914,28 +943,29 @@ export class TreeView extends React.Component<TreeViewProps> { pinToPres={emptyFunction} onClick={this.onChildClick} onDoubleClick={this.onChildDoubleClick} - dropAction={this.props.dropAction} + dragAction={this.props.dragAction} moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} - NativeHeight={this.return18} + NativeHeight={return18} NativeWidth={returnZero} + shouldNotScale={returnTrue} PanelWidth={this.titleWidth} - PanelHeight={this.return18} + PanelHeight={return18} contextMenuItems={this.contextMenuItems} renderDepth={1} - isContentActive={this.props.isContentActive} + isContentActive={emptyFunction} //this.props.isContentActive} isDocumentActive={this.props.isContentActive} focus={this.refocus} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} bringToFront={emptyFunction} - disableDocBrushing={this.props.treeView.props.disableDocBrushing} + disableBrushing={this.props.treeView.props.disableBrushing} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)} yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> ); @@ -946,10 +976,12 @@ 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, 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, - pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined, + pointerEvents: !this.props.isContentActive() ? 'none' : undefined, }}> {view} </div> @@ -971,6 +1003,12 @@ export class TreeView extends React.Component<TreeViewProps> { onPointerDown={this.ignoreEvent} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <div + className="treeView-background" + style={{ + background: StrCast(Doc.UserDoc().userColor), + }} + /> {contents} </div> {this.renderBorder} @@ -1009,8 +1047,8 @@ export class TreeView extends React.Component<TreeViewProps> { treeViewDoc={this.props.treeView?.props.Document} rootSelected={returnTrue} docViewPath={this.props.treeView.props.docViewPath} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={this.props.addDocument} moveDocument={this.move} @@ -1020,7 +1058,7 @@ export class TreeView extends React.Component<TreeViewProps> { yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} addDocTab={this.props.addDocTab} pinToPres={this.props.treeView.props.pinToPres} - disableDocBrushing={this.props.treeView.props.disableDocBrushing} + disableBrushing={this.props.treeView.props.disableBrushing} bringToFront={returnFalse} scriptContext={this} /> @@ -1030,7 +1068,7 @@ export class TreeView extends React.Component<TreeViewProps> { // renders the text version of a document as the header. This is used in the file system mode and in other vanilla tree views. @computed get renderTitleAsHeader() { - return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? ( + return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? ( <div></div> ) : ( <> @@ -1131,7 +1169,7 @@ export class TreeView extends React.Component<TreeViewProps> { add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, - dropAction: dropActionType, + dragAction: dropActionType, addDocTab: (doc: Doc, where: OpenWhere) => boolean, styleProvider: undefined | StyleProviderFunc, screenToLocalXf: () => Transform, @@ -1181,7 +1219,7 @@ export class TreeView extends React.Component<TreeViewProps> { TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined; Doc.AddDocToList(newParent, fieldKey, child, addAfter, false); newParent.treeViewOpen = true; - child.embedContainer = treeView.Document; + Doc.SetContainer(child, treeView.Document); } }; const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); @@ -1190,7 +1228,7 @@ export class TreeView extends React.Component<TreeViewProps> { const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); - return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + return aspect ? Math.min(childLayout[Width](), rowWidth()) / aspect : childLayout[Height](); }; return ( <TreeView @@ -1210,14 +1248,14 @@ export class TreeView extends React.Component<TreeViewProps> { onCheckedClick={onCheckedClick} onChildClick={onChildClick} renderDepth={renderDepth} - removeDoc={StrCast(treeViewParent.freezeChildren).includes('remove') ? undefined : remove} + removeDoc={StrCast(treeViewParent.treeViewFreezeChildren).includes('remove') ? undefined : remove} addDocument={addDocument} styleProvider={styleProvider} panelWidth={rowWidth} panelHeight={rowHeight} dontRegisterView={dontRegisterView} moveDocument={move} - dropAction={dropAction} + dragAction={dragAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} isContentActive={isContentActive} @@ -1238,6 +1276,6 @@ export class TreeView extends React.Component<TreeViewProps> { ScriptingGlobals.add(function TreeView_addNewFolder() { TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; - const opts = { title: 'Untitled folder', _stayInCollection: true, isFolder: true }; + const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }; return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id)); }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index fee4705e6..1e76d373c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,4 +1,5 @@ -import { Doc, Field, FieldResult, HeightSym, WidthSym } from '../../../../fields/Doc'; +import { Doc, Field, FieldResult } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; import { RefField } from '../../../../fields/RefField'; @@ -90,8 +91,8 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc docMap.set(layout[Id], { x: NumCast(layout.x), y: NumCast(layout.y), - width: layout[WidthSym](), - height: layout[HeightSym](), + width: layout[Width](), + height: layout[Height](), pair: { layout, data }, transition: 'all .3s', replica: '', @@ -105,8 +106,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)]; const burstScale = NumCast(pivotDoc._starburstDocScale, 1); childPairs.forEach(({ layout, data }, i) => { - const aspect = layout[HeightSym]() / layout[WidthSym](); - const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect) * burstScale; + const aspect = layout[Height]() / layout[Width](); + const docSize = Math.min(Math.min(400, layout[Width]()), Math.min(400, layout[Width]()) / aspect) * burstScale; const deg = (i / childPairs.length) * Math.PI * 2; docMap.set(layout[Id], { x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)), diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 68ba3d368..fb8ec93b2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { CssSym, Doc, Field } from '../../../../fields/Doc'; +import { Doc, Field } from '../../../../fields/Doc'; +import { DocCss } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; @@ -35,10 +36,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo () => [ this.props.A.props.ScreenToLocalTransform(), Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[CssSym], + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[DocCss], this.props.B.props.ScreenToLocalTransform(), Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[CssSym], + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[DocCss], ], action(() => { this._start = Date.now(); @@ -58,7 +59,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo 0 ); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout( - action(() => (!LinkDocs.length || !linkDoc.layout_linkDisplay) && (this._opacity = 0.05)), + action(() => (!LinkDocs.length || !linkDoc.link_displayLine) && (this._opacity = 0.05)), 750 ); // this will unhighlight the link line. const a = A.ContentDiv.getBoundingClientRect(); @@ -76,7 +77,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement(); if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return; if (!targetAhyperlink) { - if (linkDoc.layout_autoMoveAnchors) { + if (linkDoc.link_autoMoveAnchors) { linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100; linkDoc.link_anchor_1_y = ((apt.point.y - atop) / aheight) * 100; } @@ -91,7 +92,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo else linkDoc.opacity = 1; } if (!targetBhyperlink) { - if (linkDoc.layout_autoMoveAnchors) { + if (linkDoc.link_autoMoveAnchors) { linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100; linkDoc.link_anchor_2_y = ((bpt.point.y - btop) / bheight) * 100; } @@ -234,6 +235,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo bActive, textX, textY, + // 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], }; @@ -246,7 +250,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>; - const linkColorList = Doc.UserDoc().linkColorList as List<string>; + 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); @@ -261,11 +265,11 @@ 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.layout_linkDisplayArrow === undefined) { - link.layout_linkDisplayArrow = false; + if (link.link_displayArrow === undefined) { + link.link_displayArrow = false; } - return link.opacity === 0 || !a.width || !b.width || (!link.layout_linkDisplay && !aActive && !bActive) ? null : ( + return link.opacity === 0 || !a.width || !b.width || (!link.link_displayLine && !aActive && !bActive) ? null : ( <> <defs> <marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> @@ -294,7 +298,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }} onClick={this.onClickLine} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} - markerEnd={link.layout_linkDisplayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} + markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} /> {textX === undefined || !linkDescription ? null : ( <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss index cb5cef29c..4ada1731f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -1,11 +1,13 @@ -.collectionfreeformlinksview-svgCanvas{ +// TODO: change z-index to -1 when a modal is active? + +.collectionfreeformlinksview-svgCanvas { position: absolute; top: 0; left: 0; - width: 100%; + width: 100%; height: 100%; pointer-events: none; - } - .collectionfreeformlinksview-container { +} +.collectionfreeformlinksview-container { pointer-events: none; - }
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4d18ebeea..e1455525e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -4,7 +4,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -417,15 +418,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @undoBatch internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) { - // dragged document is a child of this collection - if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.embedContainer !== this.props.Document) { - // if the source doc view's embedContainer isn't this same freeformcollectionlinkDragData.dragDocument.embedContainer === this.props.Document + 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' }); - this.props.addDocument?.(source); + 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(); // do nothing if link is dropped into any freeform view parent of dragged document - return true; + e.stopPropagation(); + !added && e.preventDefault(); + return added; } return false; } @@ -483,7 +485,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; } - @undoBatch @action updateClusters(_freeform_useClusters: boolean) { this.props.Document._freeform_useClusters = _freeform_useClusters; @@ -534,7 +535,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } - @undoBatch @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); @@ -637,7 +637,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case InkTool.None: if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false); } break; } @@ -709,7 +709,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case GestureUtils.Gestures.Rectangle: if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); - const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color); + const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color); const sets = setDocs.map(sd => { return Cast(sd.text, RichTextField)?.Text as string; }); @@ -721,9 +721,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const inks = this.getActiveDocuments().filter(doc => { if (doc.type === 'ink') { const l = NumCast(doc.x); - const r = l + doc[WidthSym](); + const r = l + doc[Width](); const t = NumCast(doc.y); - const b = t + doc[HeightSym](); + const b = t + doc[Height](); const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); return pass; } @@ -870,9 +870,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection Doc.ActiveTool = InkTool.None; } else { if (this.tryDragCluster(e, this._hitCluster)) { + e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over return true; } - this.pan(e); + // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) + if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.getLocalTransform().inverse().Scale) { + this.pan(e); + e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning + } } return false; }; @@ -1017,7 +1022,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action zoom = (pointX: number, pointY: number, deltaY: number): void => { - if (this.Document._isGroup) return; + if (this.Document._isGroup || this.Document._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); @@ -1025,12 +1030,12 @@ 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_scaleMin, 1) && this.isAnnotationOverlay) { + if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { this.setPan(0, 0); return; } - if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scaleMin, 1) && this.isAnnotationOverlay) { - deltaScale = NumCast(this.rootDoc._freeform_scaleMin, 1) / invTransform.Scale; + if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { + deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale; } const localTransform = invTransform.scaleAbout(deltaScale, x, y); @@ -1045,33 +1050,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onPointerWheel = (e: React.WheelEvent): void => { if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom PresBox.Instance?.pauseAutoPres(); - if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; + if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); + const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling + 1e-4; switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if ctrl is selected then zoom - if (e.ctrlKey) { - if (this.props.isContentActive(true)) { - if (this.props.isAnnotationOverlayScrollable) { - // bcz: zooming on a webbox doesn't get the correct coordinates here for unknown reasons. - // so better to do nothing than having things jump around. - } else this.zoom(e.screenX, e.screenY, e.deltaY); - } - } // otherwise pan - else if (this.props.isContentActive(true)) { - const dx = -e.deltaX; - const dy = -e.deltaY; - if (e.shiftKey) { - !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 }); - } else { - !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy }); - } + if (!e.ctrlKey && this.props.isContentActive(true)) { + this.scrollPan({ deltaX: -e.deltaX, deltaY: e.shiftKey ? 0 : -Math.max(-1, Math.min(1, e.deltaY)) }); + break; } - break; default: case freeformScrollMode.Zoom: - if (this.props.isContentActive(true)) { - !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + if ((e.ctrlKey || !scrollable) && this.props.isContentActive(true)) { + this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + e.preventDefault(); } break; } @@ -1114,26 +1108,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2); } } - if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) { + if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) { this.setPanZoomTransition(panTime); - const scale = this.getLocalTransform().inverse().Scale; - const minScale = NumCast(this.rootDoc._freeform_scaleMin, 1); - const minPanX = NumCast(this.rootDoc._freeform_panXMin, 0); - const minPanY = NumCast(this.rootDoc._freeform_panYMin, 0); - const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._freeform_panXMax, this.nativeWidth), Math.max(minPanX, panX)); + const minScale = NumCast(this.rootDoc._freeform_scale_min, 1); + const scale = 1 - minScale / this.getLocalTransform().inverse().Scale; + const minPanX = NumCast(this.rootDoc._freeform_panX_min, 0); + const minPanY = NumCast(this.rootDoc._freeform_panY_min, 0); + const maxPanX = NumCast(this.rootDoc._freeform_panX_max, this.nativeWidth); + const newPanX = Math.min(minPanX + scale * maxPanX, Math.max(minPanX, panX)); const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale; const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight; const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight(); const maxPanY = minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth - (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) + + scale * NumCast(this.rootDoc._panY_max, nativeHeight) + (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); if (false && NumCast(this.rootDoc.layout_scrollTop) && NumCast(this.rootDoc._freeform_scale, minScale) !== minScale) { const relTop = NumCast(this.rootDoc.layout_scrollTop) / maxScrollTop; this.rootDoc.layout_scrollTop = undefined; newPanY = minPanY + relTop * (maxPanY - minPanY); - } else if (fitYscroll && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) { + } else if (fitYscroll > 2 && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) { const maxPanY = minPanY + fitYscroll; const relTop = (panY - minPanY) / (maxPanY - minPanY); setTimeout(() => (this.rootDoc.layout_scrollTop = relTop * maxScrollTop), 10); @@ -1147,7 +1142,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action nudge = (x: number, y: number, nudgeTime: number = 500) => { const collectionDoc = this.props.docViewPath().lastElement().rootDoc; - if (collectionDoc?._viewType !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) { + if (collectionDoc?._type_collection !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) { this.setPan( NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), @@ -1208,7 +1203,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]()); + const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[Width](), NumCast(doc.y) + layoutdoc[Height]()); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale !== undefined) { @@ -1246,7 +1241,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + isContentActive = () => this.props.isContentActive(); @undoBatch @action @@ -1258,7 +1253,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const layout_fieldKey = StrCast(docView.LayoutFieldKey); const newDoc = Doc.MakeCopy(docView.rootDoc, true); const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; if (below) newDoc.y = NumCast(docView.rootDoc.y) + NumCast(docView.rootDoc._height) + 10; else newDoc.x = NumCast(docView.rootDoc.x) + NumCast(docView.rootDoc._width) + 10; if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) { @@ -1269,13 +1264,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - pointerEvents = () => { + @computed get _pointerEvents() { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); const pointerEvents = DocumentDecorations.Instance.Interacting ? 'none' - : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); + : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); return pointerEvents; - }; + } + pointerEvents = () => this._pointerEvents; + childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; const childData = entry.pair.data; @@ -1298,13 +1295,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDoubleClick={this.onChildDoubleClickHandler} onBrowseClick={this.onBrowseClickHandler} ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform} - PanelWidth={childLayout[WidthSym]} - PanelHeight={childLayout[HeightSym]} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + PanelWidth={childLayout[Width]} + PanelHeight={childLayout[Height]} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} - isContentActive={this.props.childContentsActive ?? emptyFunction} + isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive} + isContentActive={this.childContentsActive} focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} addDocTab={this.addDocTab} addDocument={this.props.addDocument} @@ -1314,16 +1311,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} docViewPath={this.props.docViewPath} styleProvider={this.getClusterColor} - canEmbedOnDrag={this.props.childCanEmbedOnDrag} + dragAction={(this.rootDoc.childDragAction ?? this.props.childDragAction) as dropActionType} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} layout_showTitle={this.props.childlayout_showTitle} dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} pointerEvents={this.pointerEvents} - //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} - //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this + //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeViewFreezeChildDimensions)} // bcz: check this /> ); } @@ -1362,6 +1357,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _lightboxDoc: Opt<Doc>; getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { + const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); const childDoc = params.pair.layout; const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values @@ -1372,11 +1368,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection layoutFrameNumber === undefined ? { _width: Cast(childDocLayout._width, 'number'), _height: Cast(childDocLayout._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber); + // prettier-ignore + const rotation = Cast(_rotation,'number', + !this.layoutDoc._rotation_jitter ? null + : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) ); return { x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x), y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y), z: Cast(z, 'number'), - rotation: Cast(_rotation, 'number'), + rotation: rotation, color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.getClusterColor(childDoc, this.props, StyleProp.BackgroundColor), opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), @@ -1441,21 +1441,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }.bind(this) ); - childPositionProviderUnmemoized = (doc: Doc, replica: string) => { - return this._layoutPoolData.get(doc[Id] + (replica || '')); - }; + childPositionProviderUnmemoized = (doc: Doc, replica: string) => this._layoutPoolData.get(doc[Id] + (replica || '')); childDataProvider = computedFn( function childDataProvider(this: any, doc: Doc, replica: string) { - return this._layoutPoolData.get(doc[Id] + (replica || '')); + return this.childPositionProviderUnmemoized(doc, replica); }.bind(this) ); - childSizeProviderUnmemoized = (doc: Doc, replica: string) => { - return this._layoutSizeData.get(doc[Id] + (replica || '')); - }; + childSizeProviderUnmemoized = (doc: Doc, replica: string) => this._layoutSizeData.get(doc[Id] + (replica || '')); childSizeProvider = computedFn( function childSizeProvider(this: any, doc: Doc, replica: string) { - return this._layoutSizeData.get(doc[Id] + (replica || '')); + return this.childSizeProviderUnmemoized(doc, replica); }.bind(this) ); @@ -1541,8 +1537,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type) - const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc); + const anchor = Docs.Create.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presTransition: 500, annotationOn: this.rootDoc }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc); if (addAsAnnotation) { if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { @@ -1565,14 +1561,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.groupBounds = reaction( () => { if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) { - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[Width](), height: cd[Height]() })); return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } return undefined; }, cbounds => { if (cbounds) { - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[Width]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[Height]() / 2]; const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])]; const pbounds = { x: cbounds.x - p[0] + c[0], @@ -1638,8 +1634,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection CollectionFreeFormView.UpdateIcon( this.layoutDoc[Id] + '-icon' + new Date().getTime(), this.props.docViewPath().lastElement().ContentDiv!, - this.layoutDoc[WidthSym](), - this.layoutDoc[HeightSym](), + this.layoutDoc[Width](), + this.layoutDoc[Height](), this.props.PanelWidth(), this.props.PanelHeight(), 0, @@ -1698,7 +1694,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef) { + if (!this.layoutDoc._freeform_noAutoPan && !this.props.renderDepth && this._marqueeRef) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef?.getBoundingClientRect(); @@ -1763,12 +1759,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return; } !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); - appearanceItems.push({ description: (Doc.UserDoc().defaultToFlashcards ? 'Disable' : 'Enable') + ' Flashcard Notes', event: () => (Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards), icon: 'eye' }); - appearanceItems.push({ - description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`, - event: () => (this.Document._freeform_fitContentsToBox = !this.fitContentsToBox), - icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt', - }); appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); @@ -1851,10 +1841,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRender = action(() => { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); + const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); const loadIncrement = 5; - for (var i = 0; i < Math.min(unrendered.length, loadIncrement); i++) { - this._renderCutoffData.set(unrendered[i][Id] + '', true); + for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) { + this._renderCutoffData.set(layout_unrendered[i][Id] + '', true); } } this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1); @@ -1973,6 +1963,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30); lightboxPanelHeight = () => Math.max(0, this.props.PanelHeight() - 30); lightboxScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-15, -15); + onPassiveWheel = (e: WheelEvent) => { + const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling; + this.props.isSelected() && !scrollable && e.preventDefault(); + }; + _oldWheel: any; render() { TraceMobx(); return ( @@ -1980,8 +1976,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection className="collectionfreeformview-container" ref={r => { this.createDashEventsTarget(r); + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; // prevent wheel events from passivly propagating up through containers - !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false }); + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} onWheel={this.onPointerWheel} onClick={this.onClick} @@ -1991,12 +1989,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: - this.props.Document.type === DocumentType.MARKER - ? 'none' // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach. - : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) - ? 'all' - : (this.props.pointerEvents?.() as any), + pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : (this.props.pointerEvents?.() as any), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, transform: `scale(${this.nativeDimScaling || 1})`, width: `${100 / (this.nativeDimScaling || 1)}%`, @@ -2016,8 +2009,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onKey={this.onKeyDown} onDoubleClick={this.onChildDoubleClickHandler} onBrowseClick={this.onBrowseClickHandler} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={this.props.childContentsActive ?? emptyFunction} @@ -2265,22 +2258,20 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { const browseTransitionTime = 500; SelectionManager.DeselectAll(); - if ( - dv.props.focus(dv.props.Document, { - willZoomCentered: true, - zoomScale: 0.8, - zoomTime: browseTransitionTime, - }) === undefined - ) { - const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - 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); - Doc.linkFollowHighlight(dv?.props.Document, false); - } else { - DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }); - } + DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + if (!focused) { + const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + let containers = dv.props.docViewPath(); + let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + for (var cont of containers) { + parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + } + 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); + Doc.linkFollowHighlight(dv?.props.Document, false); + } + }); } ScriptingGlobals.add(CollectionBrowseClick); ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index c9168d40a..0f51fe6ff 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -4,6 +4,10 @@ import { Tooltip } from '@material-ui/core'; import { observer } from 'mobx-react'; import { unimplementedFunction } from '../../../../Utils'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { IconButton } from 'browndash-components'; +import { StrCast } from '../../../../fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { computed } from 'mobx'; @observer export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -22,39 +26,44 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { MarqueeOptionsMenu.Instance = this; } + @computed get userColor() { + return StrCast(Doc.UserDoc().userColor) + } + render() { - const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 19, transform: 'translate(-2px, -2px)' }} />; + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ width: 19 }} />; const buttons = ( <> - <Tooltip key="collect" title={<div className="dash-tooltip">Create a Collection</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={this.createCollection}> - <FontAwesomeIcon icon="object-group" size="lg" /> - </button> - </Tooltip> - , - <Tooltip key="group" title={<div className="dash-tooltip">Create a Grouping</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={e => this.createCollection(e, true)}> - <FontAwesomeIcon icon="layer-group" size="lg" /> - </button> - </Tooltip> - , - <Tooltip key="summarize" title={<div className="dash-tooltip">Summarize Documents</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={this.summarize}> - <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> - </button> - </Tooltip> - , - <Tooltip key="delete" title={<div className="dash-tooltip">Delete Documents</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={this.delete}> - <FontAwesomeIcon icon="trash-alt" size="lg" /> - </button> - </Tooltip> - , - <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin selected region to trail</div>} placement="bottom"> - <button className="antimodeMenu-button" onPointerDown={this.pinWithView}> - {presPinWithViewIcon} - </button> - </Tooltip> + <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} + /> </> ); return this.getElement(buttons); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index fdff13b54..7c53bfdbe 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, Opt } from '../../../../fields/Doc'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -8,7 +9,7 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; -import { GetEffectiveAcl } from '../../../../fields/util'; +import { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util'; import { intersectRect, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents'; @@ -102,7 +103,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._lassoPts = []; }; - @undoBatch @action onKeyPress = (e: KeyboardEvent) => { //make textbox and add it to this collection @@ -111,7 +111,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.Transform.transformPoint(this._downX, this._downY); if (e.key === '?') { cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight)); - cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); } else if (e.key === 'u' && this.props.ungroup) { @@ -162,7 +161,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); }) ); - } else if (e.key === 's' && e.ctrlKey) { + } /* else if (e.key === 's' && e.ctrlKey) { e.preventDefault(); const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!; slide.x = x; @@ -171,9 +170,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined }; this.props.addDocument?.(slide); e.stopPropagation(); - } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { + }*/ else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ''; - FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch'); + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note'); this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0)); e.stopPropagation(); } @@ -327,7 +326,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this._downX = x; this._downY = y; - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); + const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } @@ -338,7 +337,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action onClick = (e: React.MouseEvent): void => { if (this.props.pointerEvents?.() === 'none') return; - if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { if (Doc.ActiveTool === InkTool.None) { if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; @@ -378,7 +377,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform'; - !this.props.isAnnotationOverlay && Doc.AddFileOrphan(Doc.GetProto(doc)); doc._freeform_panX = doc._freeform_panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); @@ -386,16 +384,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection._isGroup = makeGroup; + newCollection._dragWhenActive = makeGroup; newCollection.forceActive = makeGroup; - newCollection.enableDragWhenActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; newCollection.layout_fitWidth = true; - selected.forEach(d => (d.embedContainer = newCollection)); + selected.forEach(d => Doc.SetContainer(d, newCollection)); this.hideMarquee(); return newCollection; }); + @undoBatch @action pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); @@ -432,8 +431,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); - newCollection._panX = this.Bounds.left + this.Bounds.width / 2; - newCollection._panY = this.Bounds.top + this.Bounds.height / 2; + newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; + newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; newCollection._currentFrame = activeFrame; this.props.addDocument?.(newCollection); this.props.selectDocuments([newCollection]); @@ -508,8 +507,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }; @undoBatch - @action - summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { + summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false).map(d => { this.props.removeDocument?.(d); d.x = NumCast(d.x) - this.Bounds.left; @@ -523,7 +521,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque followLinkToggle: true, _width: 200, _height: 200, - _layoutFitContentsToBox: true, + _layout_fitContentsToBox: true, _layout_showSidebar: true, title: 'overview', }); @@ -534,19 +532,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addDocument?.(portal); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); - }; + }); @action - background = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const newCollection = this.getCollection([], undefined, undefined); - this.props.addDocument?.(newCollection); - MarqueeOptionsMenu.Instance.fadeOut(true); - this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection])); - }; - - @undoBatch - marqueeCommand = action((e: KeyboardEvent) => { + marqueeCommand = (e: KeyboardEvent) => { if (this._commandExecuted || (e as any).propagationIsStopped) { return; } @@ -557,7 +546,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(e, e.key === 'h'); e.stopPropagation(); } - if ('cbtsSpg'.indexOf(e.key) !== -1) { + if ('ctsSpg'.indexOf(e.key) !== -1) { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -565,7 +554,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (e.key === 'g') this.collection(e, true); if (e.key === 'c' || e.key === 't') this.collection(e); if (e.key === 's' || e.key === 'S') this.summary(e); - if (e.key === 'b') this.background(e); if (e.key === 'p') this.pileup(e); this.cleanupInteractions(false); } @@ -575,7 +563,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.preventDefault(); this._lassoFreehand = !this._lassoFreehand; } - }); + }; touchesLine(r1: { left: number; top: number; width: number; height: number }) { for (const lassoPt of this._lassoPts) { diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e8ae88ae5..cd8b7a0cc 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -368,7 +368,7 @@ export class CollectionGridView extends CollectionSubView() { <div className="collectionGridView-contents" ref={this.createDashEventsTarget} - style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined }} + style={{ pointerEvents: !this.props.isContentActive() ? 'none' : undefined }} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onDrop={this.onExternalDrop}> diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 3e3709827..6b3318bf3 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -17,12 +17,7 @@ .collectionLinearView-menuOpener { user-select: none; } - - &.true { - border-left: $standard-border; - background-color: $medium-blue-alt; - } - + > 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 efd73a927..2254b2e5f 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,11 +1,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -15,14 +16,17 @@ 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 * of documents, it useful for horizontal menus. It can either be expandable - * or not using the linearViewExpandable field. + * or not using the linearView_Expandable field. * It is used in the following locations: * - It is used in the popup menu on the bottom left (see docButtons() in MainView.tsx) * - It is used for the context sensitive toolbar at the top (see contMenuButtons() in CollectionMenu.tsx) @@ -39,43 +43,19 @@ export class CollectionLinearView extends CollectionSubView() { this._dropDisposer?.(); this._widthDisposer?.(); this._selectedDisposer?.(); - this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); + this.childLayoutPairs.map((pair, ind) => ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); } componentDidMount() { this._widthDisposer = reaction( - () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[WidthSym]() || this.dimension()) + tot + 4, 0) : 0), + () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearView_IsOpen ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[Width]() || this.dimension()) + tot + 4, 0) : 0), width => this.childDocs.length && (this.layoutDoc._width = width), { fireImmediately: true } ); - - this._selectedDisposer = reaction( - () => NumCast(this.layoutDoc.selectedIndex), - i => - runInAction(() => { - this._selectedIndex = i; - let selected: any = undefined; - this.childLayoutPairs.map(async (pair, ind) => { - const isSelected = this._selectedIndex === ind; - if (isSelected) { - selected = pair; - } else { - ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); - } - }); - if (selected && selected.layout) { - ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); - } - }), - { fireImmediately: true } - ); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { - //used for stacking and masonry view - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); - } + this._dropDisposer?.(); + if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); }; dimension = () => NumCast(this.rootDoc._height); @@ -87,12 +67,8 @@ export class CollectionLinearView extends CollectionSubView() { @action exitLongLinks = () => { - if (DocumentLinksButton.StartLink) { - if (DocumentLinksButton.StartLink.Document) { - action((e: React.PointerEvent<HTMLDivElement>) => { - Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - }); - } + if (DocumentLinksButton.StartLink?.Document) { + action((e: React.PointerEvent<HTMLDivElement>) => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc)); } DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -161,11 +137,17 @@ export class CollectionLinearView extends CollectionSubView() { </span> ); }; + getDisplayDoc = (doc: Doc, preview: boolean = false) => { - if (doc.icon === 'linkui') return this.getLinkUI(); - if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI(); + // hack to avoid overhead of making UndoStack,etc into DocumentView style Boxes. If the UndoStack is ever intended to become part of the persisten state of the dashboard, then this would have to change. + // prettier-ignore + switch (doc.layout) { + case '<LinkingUI>': return this.getLinkUI(); + case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI(); + case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />; + } - const nested = doc._viewType === CollectionViewType.Linear; + const nested = doc._type_collection === CollectionViewType.Linear; const hidden = doc.hidden === true; let dref: Opt<HTMLDivElement>; @@ -193,12 +175,12 @@ export class CollectionLinearView extends CollectionSubView() { moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} pinToPres={emptyFunction} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType} rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} ScreenToLocalTransform={docXf} - PanelWidth={doc[WidthSym]} - PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension} + PanelWidth={doc[Width]} + PanelHeight={nested || doc._height ? doc[Height] : this.dimension} renderDepth={this.props.renderDepth + 1} dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)} focus={emptyFunction} @@ -206,8 +188,8 @@ export class CollectionLinearView extends CollectionSubView() { docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} hideResizeHandles={true} /> @@ -218,54 +200,45 @@ export class CollectionLinearView extends CollectionSubView() { render() { const flexDir = StrCast(this.Document.flexDirection); // Specify direction of linear view content const flexGap = NumCast(this.Document.flexGap); // Specify the gap between linear view content - const isExpanded = BoolCast(this.layoutDoc.linearViewIsExpanded); + const isExpanded = BoolCast(this.layoutDoc.linearView_IsOpen); const menuOpener = ( - <label - className={`collectionlinearView-label${isExpanded ? '-expanded' : ''}`} - htmlFor={this.Document[Id] + '-input'} - style={{ boxShadow: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BoxShadow) }} - onPointerDown={StopEvent}> - <div className="collectionLinearView-menuOpener">{Cast(this.props.Document.icon, 'string', null) ?? <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />}</div> - </label> + <Toggle + text={Cast(this.props.Document.icon, 'string', null)} + icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />} + type={Type.TERT} + color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)} + onPointerDown={e => e.stopPropagation()} + toggleType={ToggleType.BUTTON} + toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)} + onClick={() => { + this.layoutDoc.linearView_IsOpen = !isExpanded; + }} + tooltip={isExpanded ? 'Close' : 'Open'} + fillWidth={true} + align={'center'} + /> ); return ( - <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}> - <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}> - {!this.props.Document.linearViewExpandable ? null : ( - <Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top"> + <div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsOpen ? undefined : 'transparent' }}> + <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension(), pointerEvents: 'all' }}> + { + <> {menuOpener} - </Tooltip> - )} - <input - id={this.Document[Id] + '-input'} - type="checkbox" - checked={isExpanded} - ref={this.addMenuToggle} - onChange={action(e => { - ScriptCast(this.Document.onClick)?.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView?.(), - }); - this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked; - })} - /> - - {!this.layoutDoc.linearViewIsExpanded ? null : ( - <div - className="collectionLinearView-content" - style={{ - height: this.dimension(), - flexDirection: flexDir as any, - gap: flexGap, - }}> - {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} - </div> - )} + {!this.layoutDoc.linearView_IsOpen ? null : ( + <div + className="collectionLinearView-content" + style={{ + height: this.dimension(), + flexDirection: flexDir as any, + gap: flexGap, + }}> + {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} + </div> + )} + </> + } </div> </div> ); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index 821c8d804..f87a06033 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -8,6 +8,11 @@ display: flex; flex-direction: column; width: 100%; + align-items: center; + + .contentFittingDocumentView { + width: unset; + } .label-wrapper { display: flex; @@ -15,7 +20,6 @@ justify-content: center; height: 20px; } - } .multiColumnResizer { @@ -30,5 +34,4 @@ transition: 0.5s background-color ease; } } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 78d3d1b6e..10532b9d9 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -5,7 +5,6 @@ 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 { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -194,11 +193,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach( - action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - }) - ); + de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d))); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.x < de.x && brect.x + brect.width > de.x) { @@ -225,6 +220,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { } }) ); + return true; } } return false; @@ -234,8 +230,15 @@ export class CollectionMulticolumnView extends CollectionSubView() { onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); - isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); - getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { + isChildContentActive = () => { + const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive; + return this.props.isContentActive?.() === false || childDocsActive === false + ? false // + : this.props.isDocumentActive?.() && childDocsActive + ? true + : undefined; + }; + getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => { return ( <DocumentView Document={layout} @@ -247,8 +250,9 @@ export class CollectionMulticolumnView extends CollectionSubView() { renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} + shouldNotScale={shouldNotScale} rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + dragAction={(this.props.Document.childDragAction ?? this.props.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} suppressSetHeight={true} @@ -259,8 +263,8 @@ export class CollectionMulticolumnView extends CollectionSubView() { hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} dontRegisterView={this.props.dontRegisterView} addDocument={this.props.addDocument} @@ -284,15 +288,19 @@ export class CollectionMulticolumnView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; + const aspect = Doc.NativeAspect(layout, undefined, true); + const width = () => this.lookupPixels(layout); + const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(height() * aspect, width())); + const docheight = () => Math.min(docwidth() / aspect, height()); const dxf = () => this.lookupIndividualTransform(layout) - .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin)) .scale(this.props.NativeDimScaling?.() || 1); - const width = () => this.lookupPixels(layout); - const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + 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, width, height)} + <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}> + {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} <WidthLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss index 79fb195e8..ec7200a03 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -9,6 +9,12 @@ display: flex; flex-direction: row; height: 100%; + align-items: center; + margin: auto; + + .contentFittingDocumentView { + height: unset; + } .label-wrapper { display: flex; @@ -16,7 +22,6 @@ justify-content: center; height: 20px; } - } .multiRowResizer { @@ -31,5 +36,4 @@ transition: 0.5s background-color ease; } } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 4d61dc272..04cfc5456 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -5,7 +5,6 @@ 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 { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -194,11 +193,7 @@ export class CollectionMultirowView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach( - action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - }) - ); + de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d))); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { @@ -225,6 +220,7 @@ export class CollectionMultirowView extends CollectionSubView() { } }) ); + return true; } } return false; @@ -234,8 +230,15 @@ export class CollectionMultirowView extends CollectionSubView() { onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); - isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); - getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { + isChildContentActive = () => { + const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive; + return this.props.isContentActive?.() === false || childDocsActive === false + ? false // + : this.props.isDocumentActive?.() && childDocsActive + ? true + : undefined; + }; + getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => { return ( <DocumentView Document={layout} @@ -247,8 +250,9 @@ export class CollectionMultirowView extends CollectionSubView() { renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} + shouldNotScale={shouldNotScale} rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + dropAction={StrCast(this.rootDoc.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} @@ -257,9 +261,10 @@ export class CollectionMultirowView extends CollectionSubView() { hideResizeHandles={this.props.childHideResizeHandles?.()} hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} + dragAction={this.props.childDragAction} focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} dontRegisterView={this.props.dontRegisterView} addDocument={this.props.addDocument} @@ -283,15 +288,19 @@ export class CollectionMultirowView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; + const aspect = Doc.NativeAspect(layout, undefined, true); + const height = () => this.lookupPixels(layout); + const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const docheight = () => Math.min(width() / aspect, height()); + const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(width(), docheight() * aspect)); const dxf = () => this.lookupIndividualTransform(layout) - .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin) - (height() - docheight()) / 2) .scale(this.props.NativeDimScaling?.() || 1); - const height = () => this.lookupPixels(layout); - const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox); collector.push( - <div className={'document-wrapper'} style={{ height: height() }} key={'wrapper' + i}> - {this.getDisplayDoc(layout, dxf, width, height)} + <div className="document-wrapper" style={{ height: height() }} key={'wrapper' + i}> + {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} <HeightLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 3a0c2c85c..52ebb7763 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -104,7 +104,7 @@ .schema-header-row { cursor: grab; - justify-content: flex-end; + //justify-content: flex-end; .row-menu { display: flex; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index bcc2ca129..babe5c810 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -8,17 +8,16 @@ import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { SelectionManager } from '../../../util/SelectionManager'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; -import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; @@ -33,6 +32,7 @@ export enum ColumnType { Date, Image, RTF, + Enumeration, Any, } @@ -43,9 +43,10 @@ export const FInfotoColType: { [key: string]: ColumnType } = { date: ColumnType.Date, image: ColumnType.Image, rtf: ColumnType.RTF, + enumeration: ColumnType.Enumeration, }; -const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate', 'text']; +const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text']; @observer export class CollectionSchemaView extends CollectionSubView() { @@ -55,6 +56,7 @@ export class CollectionSchemaView extends CollectionSubView() { private _makeNewColumn: boolean = false; private _documentOptions: DocumentOptions = new DocumentOptions(); private _tableContentRef: HTMLDivElement | null = null; + private _menuTarget = React.createRef<HTMLDivElement>(); static _rowHeight: number = 50; static _rowSingleLineHeight: number = 32; @@ -78,6 +80,11 @@ export class CollectionSchemaView extends CollectionSubView() { @observable _filterSearchValue: string = ''; @observable _selectedCell: [Doc, number] | undefined; + // target HTMLelement portal for showing a popup menu to edit cell values. + public get MenuTarget() { + return this._menuTarget.current; + } + @computed get _selectedDocs() { return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.rootDoc)); } @@ -224,7 +231,7 @@ export class CollectionSchemaView extends CollectionSubView() { @undoBatch @action - setSort = (field: string | undefined, desc: boolean = false) => { + setColumnSort = (field: string | undefined, desc: boolean = false) => { this.layoutDoc.sortField = field; this.layoutDoc.sortDesc = desc; }; @@ -337,9 +344,7 @@ export class CollectionSchemaView extends CollectionSubView() { dragColumn = (e: PointerEvent, index: number) => { const dragData = new DragManager.ColumnDragData(index); const dragEles = [this._colEles[index]]; - this.childDocs.forEach(doc => { - dragEles.push(this._rowEles.get(doc).children[1].children[index]); - }); + this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index])); DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y); document.removeEventListener('pointermove', this.highlightDropColumn); @@ -353,24 +358,28 @@ export class CollectionSchemaView extends CollectionSubView() { return true; }; - @action - highlightDropColumn = (e: PointerEvent) => { - e.stopPropagation(); - const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + findDropIndex = (mouseX: number) => { let index: number | undefined; this.displayColumnWidths.reduce((total, curr, i) => { if (total <= mouseX && total + curr >= mouseX) { - if (mouseX <= total + curr / 2) index = i; + if (mouseX <= total + curr) index = i; else index = i + 1; } return total + curr; }, CollectionSchemaView._rowMenuWidth); + return index; + }; + @action + highlightDropColumn = (e: PointerEvent) => { + e.stopPropagation(); + const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + const index = this.findDropIndex(mouseX); this._colEles.forEach((colRef, i) => { let leftStyle = ''; let rightStyle = ''; - if (i + 1 === index) rightStyle = `solid 2px ${Colors.MEDIUM_BLUE}`; - if (i === index && i === 0) leftStyle = `solid 2px ${Colors.MEDIUM_BLUE}`; + if (i + 1 === index) rightStyle = `solid 12px ${Colors.MEDIUM_BLUE}`; + if (i === index && i === 0) leftStyle = `solid 12px ${Colors.MEDIUM_BLUE}`; colRef.style.borderLeft = leftStyle; colRef.style.borderRight = rightStyle; this.childDocs.forEach(doc => { @@ -425,17 +434,9 @@ export class CollectionSchemaView extends CollectionSubView() { @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { - e.stopPropagation(); const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]; - let index = de.complete.columnDragData.colIndex; - this.displayColumnWidths.reduce((total, curr, i) => { - if (total <= mouseX && total + curr >= mouseX) { - if (mouseX <= total + curr / 2) index = i; - else index = i + 1; - } - return total + curr; - }, CollectionSchemaView._rowMenuWidth); - this.moveColumn(de.complete.columnDragData.colIndex, index); + const index = this.findDropIndex(mouseX); + this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex); this._colEles.forEach((colRef, i) => { colRef.style.borderLeft = ''; @@ -446,6 +447,7 @@ export class CollectionSchemaView extends CollectionSubView() { }); }); + e.stopPropagation(); return true; } const draggedDocs = de.complete.docDragData?.draggedDocuments; @@ -460,7 +462,6 @@ export class CollectionSchemaView extends CollectionSubView() { if (draggedView) DocumentManager.Instance.RemoveView(draggedView); DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true)); }); - e.stopPropagation(); return true; } return false; @@ -484,24 +485,11 @@ export class CollectionSchemaView extends CollectionSubView() { return false; }; - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; - const newDoc = Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true }); - FormattedTextBox.SelectOnLoad = newDoc[Id]; - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; - return this.addRow(newDoc) || false; - }; - menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); - DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true); + DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true); - ContextMenu.Instance.setDefaultItem('::', (name: string): void => { - Doc.GetProto(this.props.Document)[name] = ''; - this.addRow(Docs.Create.TextDocument('', { title: name, _layout_autoHeight: true })); - }); ContextMenu.Instance.displayMenu(x, y, undefined, true); }; @@ -621,7 +609,7 @@ export class CollectionSchemaView extends CollectionSubView() { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); }; - getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field); + getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field); removeFieldFilters = (field: string) => { this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove')); @@ -697,6 +685,12 @@ export class CollectionSchemaView extends CollectionSubView() { ); } + onPassiveWheel = (e: WheelEvent) => { + // 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._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + }; + _oldWheel: any; @computed get keysDropdown() { return ( <div className="schema-key-search"> @@ -710,16 +704,11 @@ export class CollectionSchemaView extends CollectionSubView() { </div> <div className="schema-key-list" - 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 } - ) - }> + ref={r => { + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }}> {this._menuKeys.map(key => ( <div className="schema-search-result" @@ -773,7 +762,7 @@ export class CollectionSchemaView extends CollectionSubView() { } }); - const filters = StrListCast(this.Document._docFilters); + const filters = StrListCast(this.Document._childFilters); return keyOptions.map(key => { let bool = false; if (filters !== undefined) { @@ -821,7 +810,6 @@ export class CollectionSchemaView extends CollectionSubView() { } @computed get sortedDocs() { - trace(); const field = StrCast(this.layoutDoc.sortField); const desc = BoolCast(this.layoutDoc.sortDesc); const docs = !field @@ -845,6 +833,7 @@ export class CollectionSchemaView extends CollectionSubView() { render() { return ( <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}> + <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div> <div className="schema-table" onWheel={e => this.props.isContentActive() && e.stopPropagation()} @@ -866,20 +855,28 @@ export class CollectionSchemaView extends CollectionSubView() { columnWidths={this.displayColumnWidths} sortField={this.sortField} sortDesc={this.sortDesc} - setSort={this.setSort} + setSort={this.setColumnSort} rowHeight={this.rowHeightFunc} removeColumn={this.removeColumn} resizeColumn={this.startResize} openContextMenu={this.openContextMenu} dragColumn={this.dragColumn} setColRef={this.setColRef} + isContentActive={this.props.isContentActive} /> ))} </div> {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={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} /> + <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} + /> </div> {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} {this.previewWidth > 0 && ( @@ -900,8 +897,8 @@ export class CollectionSchemaView extends CollectionSubView() { isContentActive={returnTrue} isDocumentActive={returnFalse} ScreenToLocalTransform={this.screenToLocal} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} styleProvider={DefaultStyleProvider} docViewPath={returnEmptyDoclist} @@ -946,17 +943,18 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)} Document={doc} DataDoc={dataDoc} + yPadding={index} renderDepth={this.props.schema.props.renderDepth + 1} PanelWidth={this.tableWidthFunc} PanelHeight={this.props.rowHeight} styleProvider={DefaultStyleProvider} waitForDoubleClickToClick={returnNever} defaultDoubleClick={returnIgnore} - enableDragWhenActive={true} + dragAction="move" onClickScriptDisable="always" focus={this.props.schema.focusDocument} - docFilters={this.props.schema.childDocFilters} - docRangeFilters={this.props.schema.childDocRangeFilters} + childFilters={this.props.schema.childDocFilters} + childFiltersByRanges={this.props.schema.childDocRangeFilters} searchFilterDocs={this.props.schema.searchFilterDocs} rootSelected={this.props.schema.rootSelected} ScreenToLocalTransform={this.childScreenToLocal(index)} @@ -970,7 +968,6 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP hideLinkAnchors={true} layout_fitWidth={returnTrue} scriptContext={this} - canEmbedOnDrag={true} /> </div> ); diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 7da3c042c..65e47f441 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -5,8 +5,6 @@ import { observer } from 'mobx-react'; import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { Colors } from '../../global/globalEnums'; import './CollectionSchemaView.scss'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { DragManager } from '../../../util/DragManager'; export interface SchemaColumnHeaderProps { columnKeys: string[]; @@ -14,6 +12,7 @@ export interface SchemaColumnHeaderProps { columnIndex: number; sortField: string; sortDesc: boolean; + isContentActive: (outsideReaction?: boolean | undefined) => boolean | undefined; setSort: (field: string | undefined, desc?: boolean) => void; removeColumn: (index: number) => void; rowHeight: () => number; @@ -46,7 +45,7 @@ export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> @action onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false); + this.props.isContentActive(true) && setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false); }; render() { diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 45bfe4f77..e8e606030 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc'; import { BoolCast } from '../../../../fields/Types'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { OpenWhere } from '../../nodes/DocumentView'; @@ -15,6 +15,7 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { CollectionSchemaView } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaTableCell } from './SchemaTableCell'; +import { Transform } from '../../../util/Transform'; @observer export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -52,13 +53,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { }; onPointerEnter = (e: any) => { - if (!SnappingManager.GetIsDragging()) return; - document.removeEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointermove', this.onPointerMove); + if (SnappingManager.GetIsDragging() && this.props.isContentActive()) { + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); + } }; onPointerMove = (e: any) => { - if (!SnappingManager.GetIsDragging()) return; const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.embedContainer === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false; if (this._ref && dragIsRow) { @@ -111,18 +112,18 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { }}> <div className="schema-row-button" - onPointerDown={undoBatch(e => { + onPointerDown={undoable(e => { e.stopPropagation(); this.props.removeDocument?.(this.rootDoc); - })}> + }, 'Delete Row')}> <FontAwesomeIcon icon="times" /> </div> <div className="schema-row-button" - onPointerDown={e => { + onPointerDown={undoable(e => { e.stopPropagation(); this.props.addDocTab(this.rootDoc, OpenWhere.addRight); - }}> + }, 'Open Doc on Right')}> <FontAwesomeIcon icon="external-link-alt" /> </div> </div> @@ -143,6 +144,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { selectedCell={this.selectedCell} setColumnValues={this.setColumnValues} oneLine={BoolCast(this.schemaDoc?._singleLine)} + menuTarget={this.schemaView.MenuTarget} + transform={() => { + const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index; + const x = this.schemaView?.displayColumnWidths.reduce((p, c, i) => (i <= ind ? p + c : p), 0); + const y = (this.props.yPadding ?? 0) * this.props.PanelHeight(); + return new Transform(x + CollectionSchemaView._rowMenuWidth, y, 1); + }} /> ))} </div> diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 42bf32475..1c9c0de53 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,4 +1,5 @@ -import React = require('react'); +import * as React from 'react'; +import Select, { MenuPlacement } from 'react-select'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; @@ -6,14 +7,13 @@ import DatePicker from 'react-datepicker'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { RichTextField } from '../../../../fields/RichTextField'; -import { BoolCast, Cast, DateCast, FieldValue } from '../../../../fields/Types'; +import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils'; import { FInfo } from '../../../documents/Documents'; import { DocFocusOrOpen } from '../../../util/DocumentManager'; -import { dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { OpenWhere } from '../../nodes/DocumentView'; @@ -21,7 +21,7 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; -import { ColumnType, FInfotoColType } from './CollectionSchemaView'; +import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; export interface SchemaTableCellProps { @@ -41,6 +41,9 @@ export interface SchemaTableCellProps { oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit) finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView) + options?: string[]; + menuTarget: HTMLDivElement | null; + transform: () => Transform; } @observer @@ -58,14 +61,14 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { break; } protoCount++; - doc = doc.proto; + doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; const textDecoration = color !== 'black' && parenCount ? 'underline' : ''; const fieldProps: FieldViewProps = { - docFilters: returnEmptyFilter, - docRangeFilters: returnEmptyFilter, + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, @@ -73,7 +76,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'embed', + dragAction: 'move', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, @@ -115,29 +118,29 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { contents={<FieldView {...fieldProps} />} editing={this.selected ? undefined : false} GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} - SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { + SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); } const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); this.props.finishEdit?.(); return ret; - })} + }, 'edit schema cell')} /> </div> ); } get getCellType() { + const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; const cellValue = this.props.Document[this.props.fieldKey]; if (cellValue instanceof ImageField) return ColumnType.Image; if (cellValue instanceof DateField) return ColumnType.Date; if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; - if (typeof cellValue === 'string') return ColumnType.Any; + if (typeof cellValue === 'string' && columnTypeStr !== 'enumeration') return ColumnType.Any; if (typeof cellValue === 'boolean') return ColumnType.Boolean; - const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; if (columnTypeStr && columnTypeStr in FInfotoColType) { return FInfotoColType[columnTypeStr]; } @@ -152,6 +155,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { case ColumnType.Image: return <SchemaImageCell {...this.props} />; case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />; case ColumnType.RTF: return <SchemaRTFCell {...this.props} />; + case ColumnType.Enumeration: return <SchemaEnumerationCell {...this.props} options={this.props.getFinfo(this.props.fieldKey)?.values?.map(val => val.toString())} />; case ColumnType.Date: // return <SchemaDateCell {...this.props} />; default: return this.defaultCellContent; } @@ -268,7 +272,7 @@ export class SchemaRTFCell extends React.Component<SchemaTableCellProps> { fieldProps.isContentActive = this.selectedFunc; return ( <div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}> - {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} + {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} DataDoc={this.props.Document} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} </div> ); } @@ -311,3 +315,38 @@ export class SchemaBoolCell extends React.Component<SchemaTableCellProps> { ); } } +@observer +export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> { + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + render() { + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); + const options = this.props.options?.map(facet => ({ value: facet, label: facet })); + return ( + <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}> + <div style={{ width: '100%' }}> + <Select + styles={{ + menuPortal: base => ({ + ...base, + left: 0, + top: 0, + transform: `translate(${this.props.transform().TranslateX}px, ${this.props.transform().TranslateY}px)`, + width: Number(base.width) * this.props.transform().Scale, + zIndex: 9999, + }), + }} + menuPortalTarget={this.props.menuTarget} + menuPosition={'absolute'} + placeholder={StrCast(this.props.Document[this.props.fieldKey], 'select...')} + options={options} + isMulti={false} + onChange={val => KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)} + /> + </div> + </div> + ); + } +} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 3496bb835..7b2ac5713 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -36,8 +36,8 @@ $icon-size: 28px; // fonts $sans-serif: 'Roboto', sans-serif; $large-header: 16px; -$body-text: 12px; -$small-text: 9px; +$body-text: 13px; +$small-text: 10px; // $sans-serif: "Roboto Slab", sans-serif; // misc values @@ -70,6 +70,10 @@ $DFLT_IMAGE_NATIVE_DIM: 900px; $LEFT_MENU_WIDTH: 60px; $TREE_BULLET_WIDTH: 20px; +$CAROUSEL3D_CENTER_SCALE: 1.3; +$CAROUSEL3D_SIDE_SCALE: 0.3; +$CAROUSEL3D_TOP: 15; + :export { contextMenuZindex: $contextMenu-zindex; SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; @@ -84,4 +88,7 @@ $TREE_BULLET_WIDTH: 20px; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; INK_MASK_SIZE: $INK_MASK_SIZE; MEDIUM_GRAY: $medium-gray; + CAROUSEL3D_CENTER_SCALE: $CAROUSEL3D_CENTER_SCALE; + CAROUSEL3D_SIDE_SCALE: $CAROUSEL3D_SIDE_SCALE; + CAROUSEL3D_TOP: $CAROUSEL3D_TOP; } diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 537ea1e5d..efb702564 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -12,6 +12,9 @@ interface IGlobalScss { TREE_BULLET_WIDTH: string; INK_MASK_SIZE: number; MEDIUM_GRAY: string; + CAROUSEL3D_CENTER_SCALE: string; + CAROUSEL3D_SIDE_SCALE: string; + CAROUSEL3D_TOP: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts new file mode 100644 index 000000000..256377758 --- /dev/null +++ b/src/client/views/global/globalScripts.ts @@ -0,0 +1,416 @@ +import { Colors } from 'browndash-components'; +import { runInAction, action } from 'mobx'; +import { aggregateBounds } from '../../../Utils'; +import { Doc } from '../../../fields/Doc'; +import { Width, Height } from '../../../fields/DocSymbols'; +import { InkTool } from '../../../fields/InkField'; +import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types'; +import { WebField } from '../../../fields/URLField'; +import { GestureUtils } from '../../../pen-gestures/GestureUtils'; +import { LinkManager } from '../../util/LinkManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { SelectionManager } from '../../util/SelectionManager'; +import { undoable, UndoManager } from '../../util/UndoManager'; +import { GestureOverlay } from '../GestureOverlay'; +import { InkTranscription } from '../InkTranscription'; +import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from '../InkingStroke'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; +import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; +import { WebBox } from '../nodes/WebBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; +import { DocumentType } from '../../documents/DocumentTypes'; + +ScriptingGlobals.add(function IsNoneSelected() { + return SelectionManager.Views().length <= 0; +}, 'are no document selected'); + +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function setView(view: string) { + const selected = SelectionManager.Docs().lastElement(); + selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed'); +}); + +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { + const selectedViews = SelectionManager.Views(); + if (Doc.ActiveTool !== InkTool.None) { + if (checkResult) { + return ActiveFillColor(); + } + SetActiveFillColor(color ?? 'transparent'); + } else if (selectedViews.length) { + if (checkResult) { + const selView = selectedViews.lastElement(); + const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; + const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values + const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed + return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent'; + } + selectedViews.forEach(dv => { + const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; + const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values + const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed + if (contentFrameNumber !== undefined) { + CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color }); + } else { + dv.rootDoc['_' + fieldKey] = color; + } + }); + } else { + const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : []; + if (checkResult) { + return selected.lastElement()?._backgroundColor ?? 'transparent'; + } + selected.forEach(doc => (doc._backgroundColor = color)); + } +}); + +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { + if (checkResult) { + return Doc.SharingDoc().headingColor; + } + Doc.SharingDoc().headingColor = undefined; + Doc.GetProto(Doc.SharingDoc()).headingColor = color; + Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date'); +}); + +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { + const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; + if (checkResult) { + if (NumCast(selected?.Document.z) >= 1) return true; + return false; + } + selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); +}); + +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + // prettier-ignore + const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { 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, + }], + ['snaplines', { + checkResult: (doc:Doc) => BoolCast(doc._freeform_snapLines, false), + setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines, + }], + ['viewAll', { + checkResult: (doc:Doc) => BoolCast(doc._freeform_fitContentsToBox, false), + setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, + }], + ['clusters', { + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire + checkResult: (doc:Doc) => BoolCast(doc._freeform_useClusters, false), + setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters, + }], + ['arrange', { + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire + checkResult: (doc:Doc) => BoolCast(doc._autoArrange, false), + setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, + }], + ['flashcards', { + checkResult: (doc:Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false), + setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards, + }], + ]); + + if (checkResult) { + return map.get(attr)?.checkResult(selected); + } + const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} }; + SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); + setTimeout(() => batch.end(), 100); +}); + +ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: any, checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + const selected = SelectionManager.Docs().lastElement(); + // prettier-ignore + const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([ + ['font', { + checkResult: () => RichTextMenu.Instance?.fontFamily, + setDoc: () => value && RichTextMenu.Instance.setFontFamily(value), + }], + ['highlight', { + checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight, + setDoc: () => value && RichTextMenu.Instance.setHighlight(value), + }], + ['fontColor', { + checkResult: () => RichTextMenu.Instance?.fontColor, + setDoc: () => value && RichTextMenu.Instance.setColor(value), + }], + ['alignment', { + checkResult: () => RichTextMenu.Instance.textAlign, + setDoc: () => value && editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value), + }], + ['fontSize', { + checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), + setDoc: () => { + if (typeof value === 'number') value = value.toString(); + if (value && Number(value).toString() === value) value += 'px'; + RichTextMenu.Instance.setFontSize(value); + }, + }], + ]); + + if (checkResult) { + return map.get(attr)?.checkResult(); + } + map.get(attr)?.setDoc?.(); +}); + +type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal'; +type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }]; + +ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) { + const textView = RichTextMenu.Instance?.TextView; + const editorView = textView?.EditorView; + // prettier-ignore + const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) => + [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false), + toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]); + // prettier-ignore + const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => + [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false), + 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)) }], + ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), + toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], + ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}], + ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}], + ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]] + + const map = new Map(attrs.concat(alignments).concat(listings)); + if (checkResult) { + return map.get(charStyle)?.checkResult(); + } + undoable(() => map.get(charStyle)?.toggle(), 'toggle ' + charStyle)(); +}); + +export function checkInksToGroup() { + if (Doc.ActiveTool === InkTool.Write) { + 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 + // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other + const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => { + // console.log(inkDoc.x, inkDoc.y); + }); + }); + } +} + +export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { + // TODO nda - if document being added to is a inkGrouping then we can just add to that group + if (Doc.ActiveTool === InkTool.Write) { + 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; + // loop through selected an get the bound + const bounds: { x: number; y: number; width?: number; height?: number }[] = []; + + selected.map( + action(d => { + const x = NumCast(d.x); + const y = NumCast(d.y); + const width = d[Width](); + const height = d[Height](); + bounds.push({ x, y, width, height }); + }) + ); + + const aggregBounds = aggregateBounds(bounds, 0, 0); + const marqViewRef = ffView._marqueeViewRef.current; + + // set the vals for bounds in marqueeView + if (marqViewRef) { + marqViewRef._downX = aggregBounds.x; + marqViewRef._downY = aggregBounds.y; + marqViewRef._lastX = aggregBounds.r; + marqViewRef._lastY = aggregBounds.b; + } + + selected.map( + action(d => { + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + // calculate pos based on bounds + if (marqViewRef?.Bounds) { + d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; + d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; + } + return d; + }) + ); + ffView.props.removeDocument?.(selected); + // TODO: nda - this is the code to actually get a new grouped collection + const newCollection = marqViewRef?.getCollection(selected, undefined, true); + if (newCollection) { + newCollection.height = newCollection[Height](); + newCollection.width = newCollection[Width](); + } + + // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs + newCollection && ffView.props.addDocument?.(newCollection); + // TODO: nda - will probably need to go through and only remove the unprocessed selected docs + ffView.unprocessedDocs = []; + + InkTranscription.Instance.transcribeInk(newCollection, selected, false); + }); + } + CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); +} + +function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) { + InkTranscription.Instance?.createInkGroup(); + if (checkResult) { + return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool + ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures) + ? true + : true + : false; + } + runInAction(() => { + if (GestureOverlay.Instance) { + GestureOverlay.Instance.KeepPrimitiveMode = keepPrim; + } + if (Object.values(GestureUtils.Gestures).includes(tool as any)) { + if (GestureOverlay.Instance.InkShape === tool && !keepPrim) { + Doc.ActiveTool = InkTool.None; + GestureOverlay.Instance.InkShape = undefined; + } else { + Doc.ActiveTool = InkTool.Pen; + GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures; + } + } else if (tool) { + // pen or eraser + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + Doc.ActiveTool = InkTool.None; + } else { + Doc.ActiveTool = tool as any; + GestureOverlay.Instance.InkShape = undefined; + } + } else { + Doc.ActiveTool = InkTool.None; + } + }); +} + +ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); + +// toggle: Set overlay status of selected document +ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + // prettier-ignore + const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ + ['inkMask', { + checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask())), + setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask), + setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), + }], + ['fillColor', { + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"), + setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), + setMode: () => SetActiveFillColor(StrCast(value)), + }], + [ 'strokeWidth', { + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()), + setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)), + setMode: () => SetActiveInkWidth(value.toString()), + }], + ['strokeColor', { + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()), + setInk: (doc: Doc) => (doc.color = String(value)), + setMode: () => SetActiveInkColor(StrCast(value)), + }], + ]); + + if (checkResult) { + return map.get(option)?.checkResult(); + } + map.get(option)?.setMode(); + SelectionManager.Docs() + .filter(doc => doc.type === DocumentType.INK) + .map(doc => map.get(option)?.setInk(doc)); +}); + +/** WEB + * webSetURL + **/ +ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) { + const selected = SelectionManager.Views().lastElement(); + if (selected?.rootDoc.type === DocumentType.WEB) { + if (checkResult) { + return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); + } + selected.ComponentView?.setData?.(url); + //selected.rootDoc.data = new WebField(url); + } +}); +ScriptingGlobals.add(function webForward(checkResult?: boolean) { + const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox; + if (checkResult) { + return selected?.forward(checkResult) ? undefined : 'lightGray'; + } + selected?.forward(); +}); +ScriptingGlobals.add(function webBack(checkResult?: boolean) { + const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox; + if (checkResult) { + return selected?.back(checkResult) ? undefined : 'lightGray'; + } + selected?.back(); +}); + +/** Schema + * toggleSchemaPreview + **/ +ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + const result: boolean = NumCast(selected.schema_previewWidth) > 0; + if (result) return Colors.MEDIUM_BLUE; + else return 'transparent'; + } else if (selected) { + if (NumCast(selected.schema_previewWidth) > 0) { + selected.schema_previewWidth = 0; + } else { + selected.schema_previewWidth = 200; + } + } +}); +ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (selected) { + selected._schema_singleLine = !selected._schema_singleLine; + } +}); + +/** STACK + * groupBy + */ +ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) { + SelectionManager.Docs().map(doc => (doc._text_fontFamily = key)); + const editorView = RichTextMenu.Instance.TextView?.EditorView; + if (checkResult) { + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + } + if (editorView) RichTextMenu.Instance.setFontFamily(key); + else Doc.UserDoc().fontFamily = key; +}); diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 3f6369898..65d13a6c3 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -49,7 +49,7 @@ export class LinkMenu extends React.Component<Props> { <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} /> )); - return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; + return linkItems.length ? linkItems : this.props.style ? [] : [<p key="none">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; }; render() { diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index b9d56541a..8324a97d9 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -25,7 +25,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { getBackgroundColor = (): string => { const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList); - const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); let color = 'white'; // if this link's relationship property is not default "link", set its color if (link_relationshipList) { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 5af05e491..737d675aa 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -78,7 +78,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { e, e => { const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'embed'); - dragData.removeDropProperties = ['hidden']; + dragData.dropPropertiesToRemove = ['hidden']; DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; }, @@ -170,7 +170,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { linkSrc: this.props.sourceDoc, linkDoc: this.props.linkDoc, showHeader: false, - location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], + location: [(this._drag.current?.getBoundingClientRect().left ?? 100) + 40, (this._drag.current?.getBoundingClientRect().top ?? e.clientY) + 25], noPreview: false, }) }> diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss index b20ad9476..4bfb4b0b9 100644 --- a/src/client/views/linking/LinkPopup.scss +++ b/src/client/views/linking/LinkPopup.scss @@ -4,7 +4,6 @@ top: 0; height: 200px; width: 200px; - position: absolute; // padding: 15px; border-radius: 3px; @@ -37,8 +36,4 @@ margin: auto; } } - - .searchBox-container { - background: pink; - } } diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 5f2d4a7b6..6895c0746 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -14,7 +14,6 @@ import React = require('react'); import { OpenWhere } from '../nodes/DocumentView'; interface LinkPopupProps { - showPopup: boolean; linkFrom?: () => Doc | undefined; linkCreateAnchor?: () => Doc | undefined; linkCreated?: (link: Doc) => void; @@ -44,10 +43,9 @@ export class LinkPopup extends React.Component<LinkPopupProps> { getPHeight = () => 500; render() { - const popupVisibility = this.props.showPopup ? 'block' : 'none'; const linkDoc = this.props.linkFrom ? this.props.linkFrom : undefined; return ( - <div className="linkPopup-container" style={{ display: popupVisibility }}> + <div className="linkPopup-container"> {/* <div className="linkPopup-url-container"> <input autoComplete="off" type="text" value={this.linkURL} placeholder="Enter URL..." onChange={this.onLinkChange} /> <button onPointerDown={e => this.makeLinkToURL(this.linkURL, "add:right")} @@ -70,7 +68,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> { linkSearch={true} linkCreated={this.props.linkCreated} fieldKey="data" - dropAction="move" isSelected={returnTrue} isContentActive={returnTrue} select={returnTrue} @@ -89,8 +86,8 @@ export class LinkPopup extends React.Component<LinkPopupProps> { docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss new file mode 100644 index 000000000..74fbfbb2c --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss @@ -0,0 +1,15 @@ +@import '../NewLightboxStyles.scss'; + +.newLightboxButtonMeny-container { + width: 100vw; + height: 100vh; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx new file mode 100644 index 000000000..ff17e5c12 --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx @@ -0,0 +1,50 @@ +import './ButtonMenu.scss'; +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'; + +export const ButtonMenu = (props: IButtonMenu) => { + return ( + <div className={`newLightboxButtonMenu-container`}> + <div + className="newLightboxView-navBtn" + title="toggle fit width" + onClick={e => { + e.stopPropagation(); + NewLightboxView.LightboxDoc!._fitWidth = !NewLightboxView.LightboxDoc!._fitWidth; + }}></div> + <div + className="newLightboxView-tabBtn" + title="open in tab" + onClick={e => { + e.stopPropagation(); + CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none); + SelectionManager.DeselectAll(); + NewLightboxView.SetNewLightboxDoc(undefined); + }}></div> + <div + className="newLightboxView-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; + }}></div> + <div + className="newLightboxView-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; + })}></div> + </div> + ); +}; diff --git a/src/client/views/newlightbox/ButtonMenu/index.ts b/src/client/views/newlightbox/ButtonMenu/index.ts new file mode 100644 index 000000000..f53a8c729 --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/index.ts @@ -0,0 +1 @@ +export * from './ButtonMenu'
\ No newline at end of file diff --git a/src/client/views/newlightbox/ButtonMenu/utils.ts b/src/client/views/newlightbox/ButtonMenu/utils.ts new file mode 100644 index 000000000..096ea87ad --- /dev/null +++ b/src/client/views/newlightbox/ButtonMenu/utils.ts @@ -0,0 +1,3 @@ +export interface IButtonMenu { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.scss b/src/client/views/newlightbox/ExploreView/ExploreView.scss new file mode 100644 index 000000000..5a8ab2f87 --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/ExploreView.scss @@ -0,0 +1,44 @@ +@import '../NewLightboxStyles.scss'; + +.exploreView-container { + width: 100%; + height: 100%; + border-radius: 20px; + position: relative; + // transform: scale(1); + background: $gray-l1; + border-top: $standard-border; + border-color: $gray-l2; + border-radius: 0px 0px 20px 20px; + transform-origin: 50% 50%; + overflow: hidden; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $gray-l1; + } + + .exploreView-doc { + width: 60px; + height: 80px; + position: absolute; + background: $blue-l2; + // opacity: 0.8; + transform-origin: 50% 50%; + transform: translate(-50%, -50%) scale(1); + cursor: pointer; + transition: 0.2s ease; + overflow: hidden; + font-size: 9px; + padding: 10px; + border-radius: 5px; + + &:hover { + transform: translate(calc(-50% * 1.125), calc(-50% * 1.125)) scale(1.5); + } + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx new file mode 100644 index 000000000..a1d6375c4 --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx @@ -0,0 +1,32 @@ +import './ExploreView.scss'; +import { IBounds, IExploreView, emptyBounds } from './utils'; +import { IRecommendation } from '../components'; +import * as React from 'react'; +import { NewLightboxView } from '../NewLightboxView'; +import { StrCast } from '../../../../fields/Types'; + +export const ExploreView = (props: IExploreView) => { + const { recs, bounds = emptyBounds } = props; + + return ( + <div className={`exploreView-container`}> + {recs && + recs.map(rec => { + const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x)); + const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y)); + if (rec.embedding) { + const x = (rec.embedding.x / x_bound) * 50; + const y = (rec.embedding.y / y_bound) * 50; + return ( + <div className={`exploreView-doc`} onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}> + {rec.title} + </div> + ); + } else return null; + })} + <div className={`exploreView-doc`} style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}> + {StrCast(NewLightboxView.LightboxDoc?.title)} + </div> + </div> + ); +}; diff --git a/src/client/views/newlightbox/ExploreView/index.ts b/src/client/views/newlightbox/ExploreView/index.ts new file mode 100644 index 000000000..bf94eedcd --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/index.ts @@ -0,0 +1 @@ +export * from './ExploreView'
\ No newline at end of file diff --git a/src/client/views/newlightbox/ExploreView/utils.ts b/src/client/views/newlightbox/ExploreView/utils.ts new file mode 100644 index 000000000..7d9cf226d --- /dev/null +++ b/src/client/views/newlightbox/ExploreView/utils.ts @@ -0,0 +1,20 @@ +import { IRecommendation } from "../components"; + +export interface IExploreView { + recs?: IRecommendation[], + bounds?: IBounds +} + +export const emptyBounds = { + max_x: 0, + max_y: 0, + min_x: 0, + min_y: 0 +} + +export interface IBounds { + max_x: number, + max_y: number, + min_x: number, + min_y: number +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/LightboxHeader.scss b/src/client/views/newlightbox/Header/LightboxHeader.scss new file mode 100644 index 000000000..a9e60ea98 --- /dev/null +++ b/src/client/views/newlightbox/Header/LightboxHeader.scss @@ -0,0 +1,71 @@ +@import '../NewLightboxStyles.scss'; + +.newLightboxHeader-container { + width: 100%; + height: 100%; + background: $gray-l1; + border-radius: 20px 20px 0px 0px; + padding: 20px; + display: grid; + grid-template-columns: 70% 30%; + grid-template-rows: 50% 50%; + + .title-container, + .type-container { + display: flex; + flex-direction: row; + gap: 5px; + justify-content: flex-start; + align-items: center; + } + + .title-container { + grid-column: 1; + grid-row: 1; + } + + .type-container { + grid-column: 1; + grid-row: 2; + .type { + padding: 2px 7px !important; + background: $gray-l2; + } + } + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + } + + .lb-button { + border: solid 1.5px black; + padding: 3px 5px; + cursor: pointer; + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + transition: 0.2s ease; + gap: 5px; + font-size: $body-size; + height: fit-content; + + &:hover { + background: $gray-l2; + } + + &.true { + background: $blue-l1; + } + } + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx new file mode 100644 index 000000000..a272ce294 --- /dev/null +++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx @@ -0,0 +1,62 @@ +import './LightboxHeader.scss'; +import * as React from 'react'; +import { INewLightboxHeader } from "./utils"; +import { NewLightboxView } from '../NewLightboxView'; +import { StrCast } from '../../../../fields/Types'; +import { EditableText } from '../components/EditableText'; +import { getType } from '../utils'; +import { Button, IconButton, Size, Type } from 'browndash-components'; +import { MdExplore, MdTravelExplore } from 'react-icons/md' +import { BsBookmark, BsBookmarkFill } from 'react-icons/bs' +import { Doc } from '../../../../fields/Doc'; +import { LightboxView } from '../../LightboxView'; +import { Colors } from '../../global/globalEnums'; + + +export const NewLightboxHeader = (props: INewLightboxHeader) => { + const {height = 100, width} = props; + const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc) + const [editing, setEditing] = React.useState<boolean>(false) + const [title, setTitle] = React.useState<JSX.Element | null>( + (null) + ) + React.useEffect(() => { + let lbDoc = LightboxView.LightboxDoc + setDoc(lbDoc) + if (lbDoc) { + setTitle( + <EditableText + editing={editing} + text={StrCast(lbDoc.title)} + onEdit={(newText: string) => { + if(lbDoc) lbDoc.title = newText; + }} + setEditing={setEditing} + />) + } + }, [LightboxView.LightboxDoc]) + + const [saved, setSaved] = React.useState<boolean>(false) + + if (!doc) return null + else return <div className={`newLightboxHeader-container`} onPointerDown={(e) => e.stopPropagation()} style={{ minHeight: height, height: height, width: width }}> + <div className={`title-container`}> + <div className={`lb-label`}>Title</div> + {title} + </div> + <div className={`type-container`}> + <div className={`lb-label`}>Type</div> + <div className={`type`}>{getType(StrCast(doc.type))}</div> + </div> + <div style={{gridColumn: 2, gridRow: 1, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}> + <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/> + <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/> + </div> + <div style={{gridColumn: 2, gridRow: 2, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}> + <Button onClick={() => { + console.log(NewLightboxView.ExploreMode) + NewLightboxView.SetExploreMode(!NewLightboxView.ExploreMode) + }} size={Size.XSMALL} color={Colors.DARK_GRAY} type={Type.SEC} text={"t-SNE 2D Embeddings"} icon={<MdTravelExplore/>}/> + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/index.ts b/src/client/views/newlightbox/Header/index.ts new file mode 100644 index 000000000..090677c16 --- /dev/null +++ b/src/client/views/newlightbox/Header/index.ts @@ -0,0 +1 @@ +export * from './LightboxHeader'
\ No newline at end of file diff --git a/src/client/views/newlightbox/Header/utils.ts b/src/client/views/newlightbox/Header/utils.ts new file mode 100644 index 000000000..22e0487c2 --- /dev/null +++ b/src/client/views/newlightbox/Header/utils.ts @@ -0,0 +1,4 @@ +export interface INewLightboxHeader { + height?: number + width?: number +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/NewLightboxStyles.scss b/src/client/views/newlightbox/NewLightboxStyles.scss new file mode 100644 index 000000000..ff4a6c971 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxStyles.scss @@ -0,0 +1,73 @@ +$white: white; +$black: black; + +// gray +$gray-l1: rgba(230, 230, 230, 1); +$gray-l2: rgb(201, 201, 201); +$gray-l3: rgba(87, 87, 87, 1); + +// blue +$blue-l1: #cfe2f3; +$blue-l2: #6fa8dc; +$blue-l3: #0b5394; +$blue-l4: #073763; + +// view backgrounds +$background-dm: black; +$background-lm: white; +$header-dm: $gray-l3; +$header-lm: $gray-l1; + +// border +$standard-border: solid 2px; + +// standard shadow + + +$text-color-dm: $gray-l1; +$text-color-lm: $gray-l3; + + +// text / font +$title-size: 2rem; +$title-weight: 700; + +$h1-size: 15px; +$h1-weight: 700; + +$h2-size: 13px; +$h2-weight: 600; + + +$body-size: 10px; +$body-weight: 400; + +// header +$header-height: 40px; + +@keyframes skeleton-loading-l3 { + 0% { + background-color: rgba(128, 128, 128, 1); + } + 100% { + background-color: rgba(128, 128, 128, 0.5); + } +} + +@keyframes skeleton-loading-l2 { + 0% { + background-color: rgba(182, 182, 182, 1); + } + 100% { + background-color: rgba(182, 182, 182, 0.5); + } +} + +@keyframes skeleton-loading-l1 { + 0% { + background-color: rgba(230, 230, 230, 1); + } + 100% { + background-color: rgba(230, 230, 230, 0.5); + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/NewLightboxView.scss b/src/client/views/newlightbox/NewLightboxView.scss new file mode 100644 index 000000000..76c34bcf9 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxView.scss @@ -0,0 +1,34 @@ +@import './NewLightboxStyles.scss'; + +.newLightboxView-frame { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #474545bb; + backdrop-filter: blur(4px); + z-index: 1000; + + .app-document { + width: 100%; + height: 100%; + display: grid; + } + + .explore { + width: 100%; + height: 100%; + display: grid; + } + + .newLightboxView-contents { + position: relative; + display: flex; + flex-direction: column; + + .newLightboxView-doc { + position: relative; + } + } +} diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx new file mode 100644 index 000000000..3acbd1a32 --- /dev/null +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -0,0 +1,388 @@ +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 { InkTool } from '../../../fields/InkField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +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 { DocumentView, OpenWhere } from '../nodes/DocumentView'; +import { ExploreView } from './ExploreView'; +import { IBounds, emptyBounds } 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" +} + +interface LightboxViewProps { + PanelWidth: number; + PanelHeight: number; + maxBorder: number[]; +} + +type LightboxSavedState = { + panX: Opt<number>; + panY: Opt<number>; + scale: Opt<number>; + scrollTop: Opt<number>; + layout_fieldKey: Opt<string>; +}; +@observer +export class NewLightboxView extends React.Component<LightboxViewProps> { + @computed public static get LightboxDoc() { + return this._doc; + } + private static LightboxDocTemplate = () => NewLightboxView._layoutTemplate; + @observable private static _layoutTemplate: Opt<Doc>; + @observable private static _layoutTemplateString: Opt<string>; + @observable private static _doc: Opt<Doc>; + @observable private static _docTarget: Opt<Doc>; + @observable private static _docFilters: string[] = []; // filters + private static _savedState: Opt<LightboxSavedState>; + private static _history: Opt<{ doc: Doc; target?: Doc }[]> = []; + @observable private static _future: Opt<Doc[]> = []; + @observable private static _docView: Opt<DocumentView>; + + // keywords + @observable private static _keywords: string[] = [] + @action public static SetKeywords(kw: string[]) { + this._keywords = kw + } + @computed public static get Keywords() { + return this._keywords + } + + // query + @observable private static _query: string = '' + @action public static SetQuery(query: string) { + this._query = query + } + @computed public static get Query() { + return this._query + } + + // keywords + @observable private static _recs: IRecommendation[] = [] + @action public static SetRecs(recs: IRecommendation[]) { + this._recs = recs + } + @computed public static get Recs() { + return this._recs + } + + // bounds + @observable private static _bounds: IBounds = emptyBounds; + @action public static SetBounds(bounds: IBounds) { + this._bounds = bounds; + } + @computed public static get Bounds() { + return this._bounds; + } + + // explore + @observable private static _explore: Opt<boolean> = false; + @action public static SetExploreMode(status: Opt<boolean>) { + this._explore = status; + } + @computed public static get ExploreMode() { + return this._explore; + } + + // newLightbox sidebar status + @observable private static _sidebarStatus: Opt<string> = ""; + @action public static SetSidebarStatus(sidebarStatus: Opt<string>) { + this._sidebarStatus = sidebarStatus; + } + @computed public static get SidebarStatus() { + return this._sidebarStatus; + } + + static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = []; + @action public static SetNewLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) { + if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { + if (this._savedState.panX !== undefined) this.LightboxDoc._freeform_panX = this._savedState.panX; + if (this._savedState.panY !== undefined) this.LightboxDoc._freeform_panY = this._savedState.panY; + if (this._savedState.scrollTop !== undefined) this.LightboxDoc._layout_scrollTop = this._savedState.scrollTop; + if (this._savedState.scale !== undefined) this.LightboxDoc._freeform_scale = this._savedState.scale; + this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey; + } + if (!doc) { + this._docFilters && (this._docFilters.length = 0); + this._future = this._history = []; + Doc.ActiveTool = InkTool.None; + MainView.Instance._exploreMode = false; + } else { + const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.()); + //TabDocView.PinDoc(doc, { hidePresBox: true }); + this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]); + if (doc !== LightboxView.LightboxDoc) { + this._savedState = { + layout_fieldKey: StrCast(doc.layout_fieldKey), + panX: Cast(doc.freeform_panX, 'number', null), + panY: Cast(doc.freeform_panY, 'number', null), + scale: Cast(doc.freeform_scale, 'number', null), + scrollTop: Cast(doc.layout_scrollTop, 'number', null), + }; + } + } + if (future) { + this._future = [ + ...(this._future ?? []), + ...(this.LightboxDoc ? [this.LightboxDoc] : []), + ...future + .slice() + .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)) + .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length), + ]; + } + this._doc = doc; + this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined; + if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) { + doc.layout_fieldKey = layoutTemplate; + } + this._docTarget = target || doc; + + return true; + } + public static IsNewLightboxDocView(path: DocumentView[]) { + return (path ?? []).includes(this._docView!); + } + @computed get leftBorder() { + return Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]); + } + @computed get topBorder() { + return Math.min(this.props.PanelHeight / 4, this.props.maxBorder[1]); + } + newLightboxWidth = () => this.props.PanelWidth - 420; + newLightboxHeight = () => this.props.PanelHeight - 140; + newLightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1); + navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => { + return ( + <div + className="newLightboxView-navBtn-frame" + style={{ + display: display(), + left, + width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]), + bottom, + }}> + <div className="newLightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}> + <div style={{ height: 10 }}>{color}</div> + <FontAwesomeIcon icon={icon as any} size="3x" /> + </div> + </div> + ); + }; + public static GetSavedState(doc: Doc) { + return this.LightboxDoc === doc && this._savedState ? this._savedState : undefined; + } + + // adds a cookie to the newLightbox view - the cookie becomes part of a filter which will display any documents whose cookie metadata field matches this cookie + @action + public static SetCookie(cookie: string) { + if (this.LightboxDoc && cookie) { + this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); + } + } + public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => { + SelectionManager.DeselectAll(); + return NewLightboxView.SetNewLightboxDoc( + doc, + undefined, + [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort( + (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow) + ), + layoutTemplate + ); + }; + docFilters = () => NewLightboxView._docFilters || []; + addDocTab = NewLightboxView.AddDocTab; + @action public static Next() { + const doc = NewLightboxView._doc!; + const target = (NewLightboxView._docTarget = this._future?.pop()); + const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (targetDocView && target) { + const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement(); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); + if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target }); + } else { + if (!target && NewLightboxView.path.length) { + const saved = NewLightboxView._savedState; + if (LightboxView.LightboxDoc && saved) { + LightboxView.LightboxDoc._freeform_panX = saved.panX; + LightboxView.LightboxDoc._freeform_panY = saved.panY; + LightboxView.LightboxDoc._freeform_scale = saved.scale; + LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop; + } + const pop = NewLightboxView.path.pop(); + if (pop) { + NewLightboxView._doc = pop.doc; + NewLightboxView._docTarget = pop.target; + NewLightboxView._future = pop.future; + NewLightboxView._history = pop.history; + NewLightboxView._savedState = pop.saved; + } + } else { + NewLightboxView.SetNewLightboxDoc(target); + } + } + } + + @action public static Previous() { + const previous = NewLightboxView._history?.pop(); + if (!previous || !NewLightboxView._history?.length) { + NewLightboxView.SetNewLightboxDoc(undefined); + return; + } + const { doc, target } = NewLightboxView._history?.lastElement(); + const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc); + if (docView) { + NewLightboxView._docTarget = target; + target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); + } else { + NewLightboxView.SetNewLightboxDoc(doc, target); + } + if (NewLightboxView._future?.lastElement() !== previous.target || previous.doc) NewLightboxView._future?.push(previous.target || previous.doc); + } + @action + stepInto = () => { + NewLightboxView.path.push({ + doc: LightboxView.LightboxDoc, + target: NewLightboxView._docTarget, + future: NewLightboxView._future, + history: NewLightboxView._history, + saved: NewLightboxView._savedState, + }); + const coll = NewLightboxView._docTarget; + if (coll) { + const fieldKey = Doc.LayoutFieldKey(coll); + const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])]; + const links = LinkManager.Links(coll) + .map(link => LinkManager.getOppositeAnchor(link, coll)) + .filter(doc => doc) + .map(doc => doc!); + NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links); + } + }; + + @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>) + } + + future = () => NewLightboxView._future; + render() { + let newLightboxHeaderHeight = 100; + let downx = 0, + downy = 0; + return !LightboxView.LightboxDoc ? null : ( + <div + className="newLightboxView-frame" + onPointerDown={e => { + downx = e.clientX; + downy = e.clientY; + }} + onClick={e => { + if (Math.abs(downx - e.clientX) < 4 && Math.abs(downy - e.clientY) < 4) { + 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, + }}> + <NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} /> + {!NewLightboxView._explore ? + <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}> + {this.documentView} + </div> + : + <div className={`explore`}> + <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/> + </div> + } + </div> + <RecommendationList keywords={NewLightboxView.Keywords}/> + </div> + + </div> + ); + } +} +interface NewLightboxTourBtnProps { + navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element; + future: () => Opt<Doc[]>; + stepInto: () => void; +} +@observer +export class NewLightboxTourBtn extends React.Component<NewLightboxTourBtnProps> { + render() { + return this.props.navBtn( + '50%', + 0, + 0, + 'chevron-down', + () => (LightboxView.LightboxDoc /*&& this.props.future()?.length*/ ? '' : 'none'), + e => { + e.stopPropagation(); + this.props.stepInto(); + }, + '' + ); + } +} diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.scss b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss new file mode 100644 index 000000000..40dd47e47 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss @@ -0,0 +1,117 @@ +@import '../NewLightboxStyles.scss'; + +.recommendationlist-container { + height: calc(100% - 40px); + margin: 20px; + border-radius: 20px; + overflow-y: scroll; + + .recommendations { + height: fit-content; + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; + background: $gray-l1; + border-radius: 0px 0px 20px 20px; + } + + .header { + top: 0px; + position: sticky; + background: $gray-l1; + border-bottom: $standard-border; + border-color: $gray-l2; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + border-radius: 20px 20px 0px 0px; + padding: 20px; + z-index: 2; + gap: 10px; + color: $text-color-lm; + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + font-size: $body-size; + } + + .lb-caret { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: 5px; + cursor: pointer; + width: 100%; + user-select: none; + font-size: $body-size; + } + + .more { + width: 100%; + } + + &.dark { + color: $text-color-dm; + } + + .title { + height: 30px; + min-height: 30px; + font-size: $h1-size; + font-weight: $h1-weight; + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + .keywords { + display: flex; + flex-flow: row wrap; + gap: 5px; + + .keyword-input { + padding: 3px 7px; + background: $gray-l2; + outline: none; + border: none; + height: 21.5px; + color: $text-color-lm; + } + + .keyword { + padding: 3px 7px; + width: fit-content; + background: $gray-l2; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + gap: 10px; + font-size: $body-size; + font-weight: $body-weight; + + &.loading { + animation: skeleton-loading-l2 1s linear infinite alternate; + min-width: 70px; + height: 21.5px; + } + } + + } + } + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $gray-l1; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx new file mode 100644 index 000000000..9f3c32e4e --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx @@ -0,0 +1,196 @@ +import { GrClose } from 'react-icons/gr'; +import { IRecommendation, Recommendation } from "../components"; +import './RecommendationList.scss'; +import * as React from 'react'; +import { IRecommendationList } from "./utils"; +import { NewLightboxView } from '../NewLightboxView'; +import { DocCast, StrCast } from '../../../../fields/Types'; +import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; +import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; +import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils'; +import { IBounds } from '../ExploreView/utils'; +import { List } from '../../../../fields/List'; +import { Id } from '../../../../fields/FieldSymbols'; +import { LightboxView } from '../../LightboxView'; +import { IconButton, Size, Type } from 'browndash-components'; +import { Colors } from '../../global/globalEnums'; + +export const RecommendationList = (props: IRecommendationList) => { + const {loading, keywords} = props + const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true) + const [showMore, setShowMore] = React.useState<boolean>(false) + const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([]) + const [update, setUpdate] = React.useState<boolean>(true) + const initialRecs: IRecommendation[] = [ + {loading: true}, + {loading: true}, + {loading: true}, + {loading: true}, + {loading: true} + ]; + const [recs, setRecs] = React.useState<IRecommendation[]>(initialRecs) + + React.useEffect(() => { + const getKeywords = async () => { + let text = StrCast(LightboxView.LightboxDoc?.text) + console.log('[1] fetching keywords') + const response = await fetchKeywords(text, 5, true) + console.log('[2] response:', response) + const kw = response.keywords; + console.log(kw); + NewLightboxView.SetKeywords(kw); + if (LightboxView.LightboxDoc) { + console.log('setting keywords on doc') + LightboxView.LightboxDoc.keywords = new List<string>(kw); + setKeywordsLoc(NewLightboxView.Keywords); + } + setLoadingKeywords(false) + } + let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords) + if (!keywordsList || keywordsList.length < 2) { + setLoadingKeywords(true) + getKeywords() + setUpdate(!update) + } else { + setKeywordsLoc(keywordsList) + setLoadingKeywords(false) + setUpdate(!update) + } + }, [NewLightboxView.LightboxDoc]) + + // terms: vannevar bush, information spaces, + React.useEffect(() => { + const getRecommendations = async () => { + console.log('fetching recommendations') + let query = 'undefined' + if (keywordsLoc) query = keywordsLoc.join(',') + let src = StrCast(NewLightboxView.LightboxDoc?.text) + let dashDocs:IDocRequest[] = []; + // get linked docs + let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links) + console.log("linked docs", linkedDocs) + // get context docs (docs that are also in the collection) + // let contextDocs: Doc[] = DocListCast(DocCast(LightboxView.LightboxDoc?.context).data) + // let docId = LightboxView.LightboxDoc && LightboxView.LightboxDoc[Id] + // console.log("context docs", contextDocs) + // contextDocs.forEach((doc: Doc) => { + // if (docId !== doc[Id]){ + // dashDocs.push({ + // title: StrCast(doc.title), + // text: StrCast(doc.text), + // id: doc[Id], + // type: StrCast(doc.type) + // }) + // } + // }) + console.log("dash docs", dashDocs) + if (query !== undefined) { + const response = await fetchRecommendations(src, query, [], true) + const num_recs = response.num_recommendations + const recs = response.recommendations + const keywords = response.keywords + const response_bounds: IBounds = { + max_x: response.max_x, + max_y: response.max_y, + min_x: response.min_x, + min_y: response.min_y + } + // if (NewLightboxView.NewLightboxDoc) { + // NewLightboxView.NewLightboxDoc.keywords = new List<string>(keywords); + // setKeywordsLoc(NewLightboxView.Keywords); + // } + // console.log(response_bounds) + NewLightboxView.SetBounds(response_bounds) + const recommendations: IRecommendation[] = []; + for (const key in recs) { + console.log(key) + const title = recs[key].title; + const url = recs[key].url + const type = recs[key].type + const text = recs[key].text + const transcript = recs[key].transcript + const previewUrl = recs[key].previewUrl + const embedding = recs[key].embedding + const distance = recs[key].distance + const source = recs[key].source + const related_concepts = recs[key].related_concepts + const docId = recs[key].doc_id + related_concepts.length >= 1 && recommendations.push({ + title: title, + data: url, + type: type, + text: text, + transcript: transcript, + previewUrl: previewUrl, + embedding: embedding, + distance: Math.round(distance * 100) / 100, + source: source, + related_concepts: related_concepts, + docId: docId + }) + } + recommendations.sort((a, b) => { + if (a.distance && b.distance) { + return a.distance - b.distance + } else return 0 + }) + console.log("[rec]: ", recommendations) + NewLightboxView.SetRecs(recommendations) + setRecs(recommendations) + } + } + getRecommendations(); + }, [update]) + + + + return <div className={`recommendationlist-container`} onPointerDown={(e) => {e.stopPropagation()}}> + <div className={`header`}> + <div className={`title`}> + Recommendations + </div> + {NewLightboxView.LightboxDoc && <div style={{fontSize: 10}}> + The recommendations are produced based on the text in the document <b><u>{StrCast(NewLightboxView.LightboxDoc.title)}</u></b>. The following keywords are used to fetch the recommendations. + </div>} + <div className={`lb-label`}>Keywords</div> + {loadingKeywords ? <div className={`keywords`}> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + <div className={`keyword ${loadingKeywords && 'loading'}`}/> + </div> + : + <div className={`keywords`}> + {keywordsLoc && keywordsLoc.map((word, ind) => { + return <div className={`keyword`}> + {word} + <IconButton type={Type.PRIM} size={Size.XSMALL} color={Colors.DARK_GRAY} icon={<GrClose/>} onClick={() => { + let kw = keywordsLoc + kw.splice(ind) + NewLightboxView.SetKeywords(kw) + }}/> + </div> + })} + </div> + } + {!showMore ? + <div className={`lb-caret`} onClick={() => {setShowMore(true)}}> + More <FaCaretDown/> + </div> + : + <div className={`more`}> + <div className={`lb-caret`} onClick={() => {setShowMore(false)}}> + Less <FaCaretUp/> + </div> + <div className={`lb-label`}>Type</div> + <div className={`lb-label`}>Sources</div> + </div> + } + </div> + <div className={`recommendations`}> + {recs && recs.map((rec: IRecommendation) => { + return <Recommendation {...rec} /> + })} + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/index.ts b/src/client/views/newlightbox/RecommendationList/index.ts new file mode 100644 index 000000000..f4555c1f2 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/index.ts @@ -0,0 +1 @@ +export * from './RecommendationList'
\ No newline at end of file diff --git a/src/client/views/newlightbox/RecommendationList/utils.ts b/src/client/views/newlightbox/RecommendationList/utils.ts new file mode 100644 index 000000000..cdfff3258 --- /dev/null +++ b/src/client/views/newlightbox/RecommendationList/utils.ts @@ -0,0 +1,9 @@ +import { IRecommendation } from "../components"; + +export interface IRecommendationList { + loading?: boolean, + keywords?: string[], + recs?: IRecommendation[] + getRecs?: any +} + diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.scss b/src/client/views/newlightbox/components/EditableText/EditableText.scss new file mode 100644 index 000000000..7828538ab --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/EditableText.scss @@ -0,0 +1,34 @@ +@import '../../NewLightboxStyles.scss'; + +.lb-editableText, +.lb-displayText { + padding: 4px 7px !important; + border: $standard-border !important; + border-color: $gray-l2 !important; +} + +.lb-editableText { + -webkit-appearance: none; + overflow: hidden; + font-size: inherit; + border: none; + outline: none; + width: 100%; + margin: 0px; + padding: 0px; + box-shadow: none !important; + background: none; + + &:focus { + outline: none; + background-color: $blue-l1; + } +} + +.lb-displayText { + cursor: text !important; + width: 100%; + display: flex; + align-items: center; + font-size: inherit; +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.tsx b/src/client/views/newlightbox/components/EditableText/EditableText.tsx new file mode 100644 index 000000000..e9e7ca264 --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/EditableText.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' +import './EditableText.scss' +import { Size } from 'browndash-components' + +export interface IEditableTextProps { + text: string + placeholder?: string + editing: boolean + onEdit: (newText: string) => void + setEditing: (editing: boolean) => void + backgroundColor?: string + size?: Size + height?: number +} + +/** + * Editable Text is used for inline renaming of some text. + * It appears as normal UI text but transforms into a text input field when the user clicks on or focuses it. + * @param props + * @returns + */ +export const EditableText = (props: IEditableTextProps) => { + const { + editing, + height, + size, + text, + onEdit, + setEditing, + backgroundColor, + placeholder, + } = props + + const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { + onEdit(event.target.value) + } + + return editing ? ( + <input + style={{ background: backgroundColor, height: height }} + placeholder={placeholder} + size={1} + className="lb-editableText" + autoFocus + onChange={handleOnChange} + onBlur={() => setEditing(false)} + defaultValue={text} + ></input> + ) : ( + <input + style={{ background: backgroundColor, height: height }} + placeholder={placeholder} + size={1} + className="lb-editableText" + autoFocus + onChange={handleOnChange} + onBlur={() => setEditing(false)} + defaultValue={text} + ></input> + // <div className="lb-displayText" onClick={(e) => { + // e.stopPropagation() + // setEditing(true) + // }}>{text}</div> + ) +} diff --git a/src/client/views/newlightbox/components/EditableText/index.ts b/src/client/views/newlightbox/components/EditableText/index.ts new file mode 100644 index 000000000..e3367b175 --- /dev/null +++ b/src/client/views/newlightbox/components/EditableText/index.ts @@ -0,0 +1 @@ +export * from './EditableText' diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss new file mode 100644 index 000000000..c86c63ba0 --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss @@ -0,0 +1,176 @@ +@import '../../NewLightboxStyles.scss'; + +.recommendation-container { + width: 100%; + height: fit-content; + min-height: 180px; + border-radius: 20px; + display: grid; + grid-template-columns: 0% 100%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 0px; + padding: 10px; + cursor: pointer; + transition: 0.2s ease; + border: $standard-border; + border-color: $gray-l2; + background: white; + + &:hover { + // background: white !important; + transform: scale(1.02); + z-index: 0; + + .title { + text-decoration: underline; + } + } + + &.previewUrl { + grid-template-columns: calc(30% - 10px) 70%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 10px; + } + + &.loading { + animation: skeleton-loading-l2 1s linear infinite alternate; + border: none; + grid-template-columns: calc(30% - 10px) 70%; + grid-template-rows: auto auto auto auto auto; + gap: 5px 10px; + + .image-container, + .title, + .info, + .source, + .explainer, + .hide-rec { + animation: skeleton-loading-l3 1s linear infinite alternate; + } + + .title { + border-radius: 20px; + } + } + + .distance-container, + .type-container, + .source-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 5px; + } + + .image-container { + grid-row: 2/5; + grid-column: 1; + border-radius: 20px; + overflow: hidden; + + .image { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .title { + grid-row: 1; + grid-column: 1/3; + border-radius: 20px; + font-size: $h2-size; + font-weight: $h2-weight; + overflow: hidden; + border-radius: 0px; + min-height: 30px; + } + + .info { + grid-row: 2; + grid-column: 2; + border-radius: 20px; + display: flex; + flex-direction: row; + gap: 5px; + font-size: $body-size; + + .lb-type { + padding: 2px 7px !important; + background: $gray-l2; + } + } + + .lb-label { + color: $gray-l3; + font-weight: $h1-weight; + font-size: $body-size; + } + + .source { + grid-row: 3; + grid-column: 2; + border-radius: 20px; + font-size: $body-size; + display: flex; + justify-content: flex-start; + align-items: center; + + .lb-source { + padding: 2px 7px !important; + background: $gray-l2; + border-radius: 10px; + white-space: nowrap; + max-width: 130px; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .explainer { + grid-row: 4; + grid-column: 2; + border-radius: 20px; + font-size: 10px; + width: 100%; + background: $blue-l1; + border-radius: 0; + padding: 10px; + + .concepts-container { + display: flex; + flex-flow: row wrap; + margin-top: 3px; + gap: 3px; + .concept { + padding: 2px 7px !important; + background: $gray-l2; + } + } + } + + .hide-rec { + grid-row: 5; + grid-column: 2; + border-radius: 20px; + font-size: $body-size; + display: flex; + align-items: center; + margin-top: 5px; + gap: 5px; + justify-content: flex-end; + text-transform: underline; + } + + &.dark { + background: $black; + border-color: $white; + } + + &.light, + &.default { + background: $white; + border-color: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx new file mode 100644 index 000000000..2c2f04b9f --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { IRecommendation } from './utils'; +import './Recommendation.scss'; +import { getType } from '../../utils'; +import { FaEyeSlash } from 'react-icons/fa'; +import { NewLightboxView } from '../../NewLightboxView'; +import { DocumentManager } from '../../../../util/DocumentManager'; +import { Doc } from '../../../../../fields/Doc'; +import { Docs } from '../../../../documents/Documents'; + +export const Recommendation = (props: IRecommendation) => { + const { title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId } = props; + + return ( + <div + className={`recommendation-container ${loading && 'loading'} ${previewUrl && 'previewUrl'}`} + onClick={() => { + let doc: Doc | null = null; + if (source == 'Dash' && docId) { + const docView = DocumentManager.Instance.getDocumentViewsById(docId).lastElement(); + if (docView) { + doc = docView.rootDoc; + } + } else if (data) { + switch (type) { + case 'YouTube': + console.log('create ', type, 'document'); + doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript }); + break; + case 'Video': + console.log('create ', type, 'document'); + doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript }); + break; + case 'Webpage': + console.log('create ', type, 'document'); + doc = Docs.Create.WebDocument(data, { title: title, text: text }); + break; + case 'HTML': + console.log('create ', type, 'document'); + doc = Docs.Create.WebDocument(data, { title: title, text: text }); + break; + case 'Text': + console.log('create ', type, 'document'); + doc = Docs.Create.TextDocument(data, { title: title, text: text }); + break; + case 'PDF': + console.log('create ', type, 'document'); + doc = Docs.Create.PdfDocument(data, { title: title, text: text }); + break; + } + } + if (doc !== null) NewLightboxView.SetNewLightboxDoc(doc); + }}> + {loading ? <div className={`image-container`}></div> : previewUrl ? <div className={`image-container`}>{<img className={`image`} src={previewUrl}></img>}</div> : null} + <div className={`title`}>{title}</div> + <div className={`info`}> + {!loading && ( + <div className={`type-container`}> + <div className={`lb-label`}>Type</div> + <div className={`lb-type`}>{getType(type!)}</div> + </div> + )} + {!loading && ( + <div className={`distance-container`}> + <div className={`lb-label`}>Distance</div> + <div className={`lb-distance`}>{distance}</div> + </div> + )} + </div> + <div className={`source`}> + {!loading && ( + <div className={`source-container`}> + <div className={`lb-label`}>Source</div> + <div className={`lb-source`}>{source}</div> + </div> + )} + </div> + <div className={`explainer`}> + {!loading && ( + <div> + You are seeing this recommendation because this document also explores + <div className={`concepts-container`}> + {related_concepts?.map(val => { + return <div className={'concept'}>{val}</div>; + })} + </div> + </div> + )} + </div> + <div className={`hide-rec`}> + {!loading && ( + <> + <div>Hide Recommendation</div> + <div style={{ fontSize: 15, paddingRight: 5 }}> + <FaEyeSlash /> + </div> + </> + )} + </div> + </div> + ); +}; diff --git a/src/client/views/newlightbox/components/Recommendation/index.ts b/src/client/views/newlightbox/components/Recommendation/index.ts new file mode 100644 index 000000000..12ebf9d6e --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/index.ts @@ -0,0 +1,2 @@ +export * from './utils' +export * from './Recommendation'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Recommendation/utils.ts b/src/client/views/newlightbox/components/Recommendation/utils.ts new file mode 100644 index 000000000..796ce0eb0 --- /dev/null +++ b/src/client/views/newlightbox/components/Recommendation/utils.ts @@ -0,0 +1,23 @@ +import { DocumentType } from "../../../../documents/DocumentTypes" + +export interface IRecommendation { + loading?: boolean + type?: DocumentType | string, + data?: string, + title?: string, + text?: string, + source?: string, + previewUrl?: string, + transcript?: { + text: string, + start: number, + duration: number + }[], + embedding?: { + x: number, + y: number + }, + distance?: number, + related_concepts?: string[], + docId?: string +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss new file mode 100644 index 000000000..e541e3f3c --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss @@ -0,0 +1,82 @@ +@import '../../NewLightboxStyles.scss'; + +.skeletonDoc-container { + display: flex; + flex-direction: column; + height: calc(100% - 40px); + margin: 20px; + gap: 20px; + + .header { + width: calc(100% - 20px); + height: 80px; + background: $gray-l2; + animation: skeleton-loading-l2 1s linear infinite alternate; + display: grid; + grid-template-rows: 60% 40%; + padding: 10px; + grid-template-columns: auto auto auto auto; + border-radius: 20px; + + .title { + grid-row: 1; + grid-column: 1 / 5; + display: flex; + width: fit-content; + height: 100%; + min-width: 500px; + font-size: $title-size; + animation: skeleton-loading-l3 1s linear infinite alternate; + border-radius: 20px; + } + + .type { + display: flex; + padding: 3px 7px; + width: fit-content; + height: fit-content; + margin-top: 8px; + min-height: 15px; + min-width: 60px; + grid-row: 2; + grid-column: 1; + animation: skeleton-loading-l3 1s linear infinite alternate; + border-radius: 20px; + } + + .buttons-container { + grid-row: 1 / 3; + grid-column: 5; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + + .button { + width: 50px; + height: 50px; + border-radius: 100%; + animation: skeleton-loading-l3 1s linear infinite alternate; + } + } + + } + + .content { + width: 100%; + flex: 1; + -webkit-flex: 1; /* Chrome */ + background: $gray-l2; + animation: skeleton-loading-l2 1s linear infinite alternate; + border-radius: 20px; + } + + // &.dark { + // background: $black; + // } + + // &.light, + // &.default { + // background: $white; + // } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx new file mode 100644 index 000000000..50cee893f --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx @@ -0,0 +1,22 @@ +import './SkeletonDoc.scss'; +import { ISkeletonDoc } from "./utils"; +import * as React from 'react'; + +export const SkeletonDoc = (props: ISkeletonDoc) => { + const { type, data } = props + + return <div className={`skeletonDoc-container`}> + <div className={`header`}> + <div className={`title`}></div> + <div className={`type`}></div> + <div className={`tags`}></div> + <div className={`buttons-container`}> + <div className={`button`}></div> + <div className={`button`}></div> + </div> + </div> + <div className={`content`}> + {data} + </div> + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/index.ts b/src/client/views/newlightbox/components/SkeletonDoc/index.ts new file mode 100644 index 000000000..396b7272b --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/index.ts @@ -0,0 +1 @@ +export * from './SkeletonDoc'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/SkeletonDoc/utils.ts b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts new file mode 100644 index 000000000..81c32c328 --- /dev/null +++ b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts @@ -0,0 +1,5 @@ +import { IRecommendation } from "../Recommendation"; + +export interface ISkeletonDoc extends IRecommendation { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/Template.scss b/src/client/views/newlightbox/components/Template/Template.scss new file mode 100644 index 000000000..5b72ddaf9 --- /dev/null +++ b/src/client/views/newlightbox/components/Template/Template.scss @@ -0,0 +1,15 @@ +@import '../../NewLightboxStyles.scss'; + +.template-container { + width: 100vw; + height: 100vh; + + &.dark { + background: $black; + } + + &.light, + &.default { + background: $white; + } +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/Template.tsx b/src/client/views/newlightbox/components/Template/Template.tsx new file mode 100644 index 000000000..9c6f0f59c --- /dev/null +++ b/src/client/views/newlightbox/components/Template/Template.tsx @@ -0,0 +1,10 @@ +import './Template.scss'; +import * as React from 'react'; +import { ITemplate } from "./utils"; + +export const Template = (props: ITemplate) => { + + return <div className={`template-container`}> + + </div> +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/index.ts b/src/client/views/newlightbox/components/Template/index.ts new file mode 100644 index 000000000..36b5f3f46 --- /dev/null +++ b/src/client/views/newlightbox/components/Template/index.ts @@ -0,0 +1 @@ +export * from './Template'
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/Template/utils.ts b/src/client/views/newlightbox/components/Template/utils.ts new file mode 100644 index 000000000..965e653ec --- /dev/null +++ b/src/client/views/newlightbox/components/Template/utils.ts @@ -0,0 +1,3 @@ +export interface ITemplate { + +}
\ No newline at end of file diff --git a/src/client/views/newlightbox/components/index.ts b/src/client/views/newlightbox/components/index.ts new file mode 100644 index 000000000..3f9128690 --- /dev/null +++ b/src/client/views/newlightbox/components/index.ts @@ -0,0 +1,3 @@ +export * from './Template' +export * from './Recommendation' +export * from './SkeletonDoc'
\ No newline at end of file diff --git a/src/client/views/newlightbox/utils.ts b/src/client/views/newlightbox/utils.ts new file mode 100644 index 000000000..6016abca4 --- /dev/null +++ b/src/client/views/newlightbox/utils.ts @@ -0,0 +1,121 @@ +import { DocumentType } from "../../documents/DocumentTypes"; +import { IRecommendation } from "./components"; + +export interface IDocRequest { + id: string, + title: string, + text: string, + type: string +} + +export const fetchRecommendations = async (src: string, query: string, docs?: IDocRequest[], dummy?: boolean) => { + console.log("[rec] making request") + if (dummy) { + return { + "recommendations": dummyRecs, + "keywords": dummyKeywords, + "num_recommendations": 4, + "max_x": 100, + "max_y": 100, + "min_x": 0, + "min_y": 0 + + }; + } + const response = await fetch('http://127.0.0.1:8000/recommend', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "src": src, + "query": query, + "docs": docs + }) + }) + const data = await response.json(); + + return data; +} + +export const fetchKeywords = async (text: string, n: number, dummy?: boolean) => { + console.log("[fetchKeywords]") + if (dummy) { + return { + "keywords": dummyKeywords + }; + } + const response = await fetch('http://127.0.0.1:8000/keywords', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "text": text, + "n": n + }) + }) + const data = await response.json() + return data; +} + +export const getType = (type: DocumentType | string) => { + switch(type) { + case DocumentType.AUDIO: + return "Audio" + case DocumentType.VID: + return "Video" + case DocumentType.PDF: + return "PDF" + case DocumentType.WEB: + return "Webpage" + case "YouTube": + return "Video" + case "HTML": + return "Webpage" + default: + return "Unknown: " + type + } +} + +const dummyRecs = { + "a": { + title: 'Vannevar Bush - American Engineer', + previewUrl: 'https://cdn.britannica.com/98/23598-004-1E6A382E/Vannevar-Bush-Differential-Analyzer-1935.jpg', + type: 'web', + distance: 2.3, + source: 'www.britannica.com', + related_concepts: ['vannevar bush', 'knowledge'], + embedding: { + x: 0, + y: 0 + } + }, + "b": { + title: "From Memex to hypertext: Vannevar Bush and the mind's machine", + type: 'pdf', + distance: 5.4, + source: 'Google Scholar', + related_concepts: ['memex', 'vannevar bush', 'hypertext'], + }, + "c": { + title: 'How the hyperlink changed everything | Small Thing Big Idea, a TED series', + previewUrl: 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/b17d043f-2642-4117-a913-52204505513f/MargaretGouldStewart_2018V-embed.jpg?u%5Br%5D=2&u%5Bs%5D=0.5&u%5Ba%5D=0.8&u%5Bt%5D=0.03&quality=82w=640', + type: 'youtube', + distance: 5.3, + source: 'www.youtube.com', + related_concepts: ['User Control', 'Explanations'] + }, + "d": { + title: 'Recommender Systems: Behind the Scenes of Machine Learning-Based Personalization', + previewUrl: 'https://sloanreview.mit.edu/wp-content/uploads/2018/10/MAG-Ransbotham-Ratings-Recommendations-1200X627-1200x627.jpg', + type: 'pdf', + distance: 9.3, + source: 'www.altexsoft.com', + related_concepts: ['User Control', 'Explanations'] + } +} + +const dummyKeywords = ['user control', 'vannevar bush', 'hypermedia', 'hypertext']
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 8e83cf121..6558d215a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -233,7 +233,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField(); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); + const [{ result }] = await Networking.UploadFilesToServer({file: e.data}); if (!(result instanceof Error)) { this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } @@ -362,10 +362,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + if (Doc.IsInMyOverlay(this.rootDoc)) { newDoc.overlayX = this.rootDoc.x; newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + Doc.AddToMyOverlay(newDoc); } else { this.props.addDocument?.(newDoc); } @@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); - setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = time); + setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); playing = () => this.mediaState === media_state.Playing; diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 330cc3971..aae759702 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -2,7 +2,8 @@ import React = require('react'); import { action } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -48,7 +49,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { } render() { - const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]()); + const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[Height](), this.props.PanelWidth() / this.rootDoc[Width]()); return ( <div className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 660045a6f..a12f1c12b 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -93,4 +93,4 @@ display: flex; } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 3be2d9a77..ca5ec9389 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -2,11 +2,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; @@ -32,48 +31,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl get clipWidthKey() { return '_' + this.props.fieldKey + '_clipWidth'; } - componentDidMount() { this.props.setContentView?.(this); } - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { - // create disposers identified by disposerId to remove drag & drop listeners - this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); + this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; @undoBatch - private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { - event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; - if (droppedDocs?.length) { - this.dataDoc[fieldKey] = droppedDocs[0]; - } + const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.rootDoc, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); + Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc); + !added && e.preventDefault(); + e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + return added; } }; private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { - e.button !== 2 && + if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this._isAnyChildContentActive = true; + if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + } + }), + false, + undefined, action(() => { - // on click, animate slider movement to the targetWidth + if (this._isAnyChildContentActive) return; this._animating = 'all 200ms'; + // on click, animate slider movement to the targetWidth this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth(); setTimeout( action(() => (this._animating = '')), 200 ); - }), - false + }) ); + } }; @action @@ -86,7 +93,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + const anchor = Docs.Create.ConfigDocument({ + title: 'CompareAnchor:' + this.rootDoc.title, + // set presentation timing properties for restoring view + presTransition: 1000, + annotationOn: this.rootDoc, + }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; /* addAsAnnotation &&*/ this.addDocument(anchor); @@ -101,12 +113,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl e.stopPropagation; // prevent click event action (slider movement) in registerSliding 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) => { + this.dataDoc[which] = doc; + return true; + }; + remDoc = (doc: Doc, which: string) => { + if (this.dataDoc[which] === doc) { + this.dataDoc[which] = undefined; + return true; + } + return false; + }; + + whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this.props.styleProvider?.(doc, props, property); }; - + moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); + moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); + remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); + remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); render() { const clearButton = (which: string) => { return ( @@ -114,36 +143,35 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding onClick={e => this.clearDoc(e, which)}> - <FontAwesomeIcon className={`clear-button ${which}`} icon={'times'} size="sm" /> + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> </div> ); }; const displayDoc = (which: string) => { - const whichDoc = Cast(this.dataDoc[which], Doc, null); - // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null); - const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc; - return whichDoc ? ( + const whichDoc = DocCast(this.dataDoc[which]); + const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); + return targetDoc ? ( <> <DocumentView - ref={r => { - //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true }); - }} {...this.props} + Document={targetDoc} + DataDoc={undefined} + moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2} + removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={returnZero} NativeHeight={returnZero} - isContentActive={returnFalse} + isContentActive={emptyFunction} isDocumentActive={returnFalse} - styleProvider={this.docStyleProvider} - Document={targetDoc} - DataDoc={undefined} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + styleProvider={this._isAnyChildContentActive ? this.props.styleProvider : this.docStyleProvider} hideLinkButton={true} - pointerEvents={returnNone} + pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> {clearButton(which)} </> // placeholder image if doc is missing ) : ( <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon={'cloud-upload-alt'} size="lg" /> + <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> </div> ); }; @@ -156,7 +184,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; return ( - <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> + <div className={`comparisonBox${this.props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> {displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)} <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> {displayBox(`${this.fieldKey}_1`, 0, 0)} @@ -168,7 +196,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl left: `calc(${this.clipWidth + '%'} - 0.5px)`, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 5876efb01..d548ab9f1 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -71,14 +71,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { - const anchor = - this._chartRenderer?.getAnchor(pinProps) ?? - Docs.Create.TextanchorDocument({ - unrendered: true, - // when we clear selection -> we should have it so chartBox getAnchor returns undefined - // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) - /*put in some options*/ - }); + const anchor = !pinProps + ? this.rootDoc + : this._chartRenderer?.getAnchor(pinProps) ?? + Docs.Create.ConfigDocument({ + // when we clear selection -> we should have it so chartBox getAnchor returns undefined + // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) + /*put in some options*/ + }); anchor.presDataVizView = this.dataVizView; anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 11f62a61f..6b564b0c9 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -163,7 +163,10 @@ export class LineChart extends React.Component<LineChartProps> { // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { - const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true }); + const anchor = Docs.Create.ConfigDocument({ + // + title: 'line doc selection' + this._currSelected?.x, + }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 3816bddfa..d84e34d52 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,7 +1,7 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { AnimationSym, Doc } from '../../../../../fields/Doc'; +import { Doc } from '../../../../../fields/Doc'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils'; @@ -57,7 +57,7 @@ export class TableBox extends React.Component<TableBoxProps> { DragManager.StartAnchorAnnoDrag([header.current!], 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.layout_linkDisplay = true; + e.linkDocument.link_displayLine = true; // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; // e.annoDragData.linkSourceDoc.followLinkZoom = false; } diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index e1ff6f8eb..10bfb0c64 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never }; export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { - console.log('x axis creator being called'); g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); }; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 9546a6d38..8ac9d6804 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,6 +1,7 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react'; -import { AclPrivate, Doc, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { AclPrivate } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; import { Cast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; @@ -17,7 +18,6 @@ import { SearchBox } from '../search/SearchBox'; import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; import { YoutubeBox } from './../../apis/youtube/YoutubeBox'; import { AudioBox } from './AudioBox'; -import { FontIconBox } from './button/FontIconBox'; import { ColorBox } from './ColorBox'; import { ComparisonBox } from './ComparisonBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; @@ -25,9 +25,11 @@ import { DocumentViewProps } from './DocumentView'; import './DocumentView.scss'; import { EquationBox } from './EquationBox'; import { FieldView, FieldViewProps } from './FieldView'; +import { FontIconBox } from './FontIconBox/FontIconBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; +import { ImportElementBox } from './importBox/ImportElementBox'; import { KeyValueBox } from './KeyValueBox'; import { LabelBox } from './LabelBox'; import { LinkAnchorBox } from './LinkAnchorBox'; @@ -35,6 +37,7 @@ import { LinkBox } from './LinkBox'; import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; import { PDFBox } from './PDFBox'; +import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from './RecordingBox'; import { ScreenshotBox } from './ScreenshotBox'; import { ScriptingBox } from './ScriptingBox'; @@ -266,7 +269,9 @@ export class DocumentContentsView extends React.Component< HTMLtag, ComparisonBox, LoadingBox, + PhysicsSimulationBox, SchemaRowBox, + ImportElementBox, MapPushpinBox, }} bindings={bindings} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index d5ca30957..7723a088d 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; @@ -71,14 +71,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, this.onLinkButtonMoved, emptyFunction, - action((e, doubleTap) => doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc)), + action((e, doubleTap) => { + doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc); + }), undefined, undefined, action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) ); }; - @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, @@ -123,17 +124,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, returnFalse, emptyFunction, - undoBatch( - action(e => { - DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View); - }) - ) + action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)) ); }; - public static finishLinkClick = undoBatch( - action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => { - if (startLink === endLink) { + @undoBatch + public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) { + runInAction(() => { + if (startLink === endLink || !startLink) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; @@ -185,8 +183,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp ); } } - }) - ); + }); + } @action clearLinks() { DocumentLinksButton.StartLink = undefined; @@ -195,7 +193,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - const filters = this.props.View.props.docFilters(); + const filters = this.props.View.props.childFilters(); Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) { results.push(link); diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 1265651ad..b25540dd3 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -24,7 +24,7 @@ width: 100%; height: 100%; border-radius: inherit; - transition: outline 0.3s linear; + // transition: outline 0.3s linear; // background: $white; //overflow: hidden; transform-origin: center; @@ -115,7 +115,10 @@ width: 100%; height: 100%; transition: inherit; - + display: flex; + justify-content: center; + align-items: center; + .sharingIndicator { height: 30px; width: 30px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d8494e58e..bb9f45bdd 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,10 +3,12 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { AclPrivate, Animation, DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -35,8 +37,10 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; +import { InkingStroke } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; +import { UndoStack } from '../UndoStack'; import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; @@ -47,8 +51,7 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { InkingStroke } from '../InkingStroke'; -import { RefField } from '../../../fields/RefField'; +import { SettingsManager } from '../../util/SettingsManager'; const { Howl } = require('howler'); interface Window { @@ -66,9 +69,7 @@ export enum OpenWhere { addLeft = 'add:left', addRight = 'add:right', addBottom = 'add:bottom', - dashboard = 'dashboard', close = 'close', - fullScreen = 'fullScreen', toggle = 'toggle', toggleRight = 'toggle:right', replace = 'replace', @@ -115,6 +116,7 @@ export interface DocComponentView { brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox + addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) 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; @@ -157,16 +159,18 @@ export interface DocumentViewSharedProps { CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; PanelWidth: () => number; PanelHeight: () => number; + shouldNotScale?: () => boolean; docViewPath: () => DocumentView[]; childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt styleProvider: Opt<StyleProviderFunc>; setTitleFocus?: () => void; focus: DocFocusFunc; layout_fitWidth?: (doc: Doc) => boolean | undefined; - docFilters: () => string[]; - docRangeFilters: () => string[]; + childFilters: () => string[]; + childFiltersByRanges: () => string[]; searchFilterDocs: () => Doc[]; layout_showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; @@ -179,7 +183,7 @@ export interface DocumentViewSharedProps { pinToPres: (document: Doc, pinProps: PinProps) => void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - canEmbedOnDrag?: boolean; + dragAction?: dropActionType; treeViewDoc?: Doc; xPadding?: number; yPadding?: number; @@ -189,9 +193,8 @@ export interface DocumentViewSharedProps { hideCaptions?: boolean; ignoreAutoHeight?: boolean; forceAutoHeight?: boolean; - disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. + disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected - enableDragWhenActive?: boolean; waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; @@ -276,7 +279,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return Doc.LayoutFieldKey(this.layoutDoc); } @computed get layout_showTitle() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.layout_showTitle) as Opt<string>; + return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; } @computed get NativeDimScaling() { return this.props.NativeDimScaling?.() || 1; @@ -306,7 +309,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } @computed get layout_showCaption() { - return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.showCaption) || 0; + return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0; } @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; @@ -329,8 +332,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return ( DocumentView.LongPress || onScriptDisable === 'always' || - (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) || - this._componentView?.isAnyChildContentActive?.() + (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.())) ); } @computed get onClickHandler() { @@ -352,11 +354,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps componentDidMount() { this.setupHandlers(); } - + preDropFunc = (e: Event, de: DragManager.DropEvent) => { + const dropAction = this.layoutDoc.dropAction as dropActionType; + if (de.complete.docDragData && this.isContentActive() && !this.props.treeViewDoc) { + dropAction && (de.complete.docDragData.dropAction = dropAction); + e.stopPropagation(); + } + }; setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document, this.preDropFunc); this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)); this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); } @@ -384,7 +392,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; - dragData.canEmbed = this.props.canEmbedOnDrag; + dragData.canEmbed = this.rootDoc.dragAction ?? this.props.dragAction ? true : false; const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag( @@ -402,7 +410,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => { const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document - (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document + (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document ? true : false; return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; @@ -456,27 +464,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // instead of in the global lightbox const oldFunc = DocumentViewInternal.addDocTabFunc; DocumentViewInternal.addDocTabFunc = this.props.addDocTab; - const res = - this.onClickHandler?.script.run( - { - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), - clientX, - clientY, - shiftKey, - altKey, - metaKey, - }, - console.log - ).result?.select === true - ? this.props.select(false) - : ''; + this.onClickHandler?.script.run( + { + this: this.layoutDoc, + self: this.rootDoc, + _readOnly_: false, + scriptContext: this.props.scriptContext, + documentView: this.props.DocumentView(), + clientX, + clientY, + shiftKey, + altKey, + metaKey, + }, + console.log + ).result?.select === true + ? this.props.select(false) + : ''; DocumentViewInternal.addDocTabFunc = oldFunc; }; - clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); + clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { @@ -484,13 +491,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } preventDefault = false; } - - this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); + const sendToBack = e.altKey; + this._singleClickFunc = + clickFunc ?? + (() => + sendToBack + ? this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.bringToFront(this.rootDoc, true) + : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); - } else { + } else if (!DocumentView.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; } @@ -502,7 +514,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerDown = (e: React.PointerEvent): void => { - this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000); + this._longPressSelector = setTimeout(() => { + if (DocumentView.LongPress) { + if (this.rootDoc.undoIgnoreFields) { + runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline)); + } else { + this.props.select(false); + } + } + }, 1000); if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); this._downX = e.clientX; @@ -513,19 +533,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( // prettier-ignore - this.props.isDocumentActive?.() && + (this.props.isDocumentActive?.() || this.props.isContentActive?.()) && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && this.pointerEvents !== 'none' && - !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) + !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); + //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); // listen to move events if document content isn't active or document is draggable - if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) { + if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.rootDoc._dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } } @@ -539,7 +559,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); + this._longPressSelector && clearTimeout(this._longPressSelector); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this.props.dragAction || undefined) as dropActionType)); } }; @@ -557,8 +578,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.onPointerUpHandler?.script) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { - this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME; - if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected + this._doubleTap = (this.onDoubleClickHandler?.script || this.rootDoc.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME; + if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } if (DocumentView.LongPress) e.preventDefault(); }; @@ -598,16 +619,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; + if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; if (this.props.Document === Doc.ActiveDashboard) { + e.stopPropagation(); + e.preventDefault(); alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); - return; + return true; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); if (linkdrag.linkSourceDoc) { - e.stopPropagation(); if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } @@ -615,8 +637,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); } + e.stopPropagation(); + return true; } } + return false; }; @undoBatch @@ -654,7 +679,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) { + if (e && this.rootDoc._layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); @@ -702,10 +727,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null); const appearance = cm.findByDescription('UI Controls...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; + + if (this.props.renderDepth === 0) { + appearanceItems.push({ description: 'Open in Lightbox', event: () => LightboxView.SetLightboxDoc(this.rootDoc), icon: 'hand-point-right' }); + } !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; @@ -725,18 +754,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); - if (!this.Document.annotationOn) { - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - !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' }); - !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: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); + if (!this.props.treeViewDoc) { + if (!this.Document.annotationOn) { + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + !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' }); + !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: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); + } } } @@ -767,23 +798,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); } const constantItems: ContextMenuProps[] = []; - constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' }); if (!Doc.IsSystem(this.rootDoc)) { - constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() }); - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); + 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) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } } - cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' }); + constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' }); + cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); - !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DocData]), icon: 'hand-point-right' }); let documentationDescription: string | undefined = undefined; let documentationLink: string | undefined = undefined; @@ -818,7 +848,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps break; } // Add link to help documentation - if (documentationDescription && documentationLink) { + if (!this.props.treeViewDoc && documentationDescription && documentationLink) { helpItems.push({ description: documentationDescription, event: () => window.open(documentationLink, '_blank'), @@ -832,44 +862,50 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); }; - rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; + @computed get _rootSelected() { + return this.props.isSelected(false) || (this.props.Document.rootDocument && this.props.rootSelected?.(false)) || false; + } + rootSelected = (outsideReaction?: boolean) => this._rootSelected; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - isContentActive = (outsideReaction?: boolean): boolean | undefined => { - return this.props.isContentActive() === false + @computed get _isContentActive() { + // true - if the document has been activated directly or indirectly (by having its children selected) + // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive + // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking) + return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' ? false - : Doc.ActiveTool !== InkTool.None || - SnappingManager.GetIsDragging() || - this.rootSelected() || - this.rootDoc.forceActive || - this.props.isSelected(outsideReaction) || - this._componentView?.isAnyChildContentActive?.() || - this.props.isContentActive() + : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() ? true : undefined; - }; + } + isContentActive = (): boolean | undefined => this._isContentActive; @observable _retryThumb = 1; - thumbShown = () => { - const childHighlighted = () => - Array.from(Doc.highlightedDocs.keys()) - .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) - .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); + @computed get _thumbShown() { + const childHighlighted = () => false; + // Array.from(Doc.highlightedDocs.keys()) + // .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) + // .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc)); return !this.props.LayoutTemplateString && - !this.props.isSelected() && + !this.isContentActive() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) && - !this._componentView?.isAnyChildContentActive?.() + ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) ? true : false; - }; - docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)]; - contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents); + } + thumbShown = () => this._thumbShown; + childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; + + /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive + @computed get _contentPointerEvents() { + return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents; + } + contentPointerEvents = () => this._contentPointerEvents; @computed get contents() { TraceMobx(); const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; @@ -877,7 +913,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className="documentView-contentsView" style={{ - pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all', + pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all', height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? null : ( @@ -900,7 +936,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps docViewPath={this.props.viewPath} thumbShown={this.thumbShown} setContentView={this.setContentView} - docFilters={this.docFilters} + childFilters={this.childFilters} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} @@ -912,7 +948,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps setTitleFocus={this.setTitleFocus} layout_fieldKey={this.finalLayoutKey} /> - {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); } @@ -922,14 +958,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { // prettier-ignore switch (property.split(':')[0]) { - case StyleProp.layout_showTitle: return ''; + case StyleProp.ShowTitle: return ''; case StyleProp.PointerEvents: return 'none'; case StyleProp.Highlighting: return undefined; } return this.props.styleProvider?.(doc, props, property); }; // We need to use allrelatedLinks to get not just links to the document as a whole, but links to - // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., + // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link // - and links to PDF/Web docs at a certain scroll location never create an explicit view. @@ -940,20 +976,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps link => Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) || Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) || - ((link.link_anchor_1 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) || - ((link.link_anchor_2 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc)) + ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) || + ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc)) ); } @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - hideLink = computedFn((link: Doc) => () => (link.layout_linkDisplay = false)); + hideLink = computedFn((link: Doc) => () => (link.link_displayLine = false)); @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); - if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.layout_linkDisplay); + if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine); return filtered.map(link => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -1001,7 +1037,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps gumStream = stream; recorder = new MediaRecorder(stream); recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); + const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { const audioField = new AudioField(result.accessPaths.agnostic.client); const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null); @@ -1046,13 +1082,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get innards() { TraceMobx(); const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; - const layout_showTitle = this.layout_showTitle?.split(':')[0]; - const layout_showTitleHover = this.layout_showTitle?.includes(':hover'); + const showTitle = this.layout_showTitle?.split(':')[0]; + const showTitleHover = this.layout_showTitle?.includes(':hover'); const captionView = !this.layout_showCaption ? null : ( <div className="documentView-captionWrapper" style={{ - pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + pointerEvents: this.rootDoc.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, minWidth: 50 * ffscale(), maxHeight: `max(100%, ${20 * ffscale()}px)`, }}> @@ -1071,27 +1107,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps /> </div> ); - const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; + const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; const background = StrCast( - SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, - Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' + SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, + Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().headingColor) : SettingsManager.Instance.userVariantColor ); - const layout_sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); - const titleView = !layout_showTitle ? null : ( + const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); + const titleView = !showTitle ? null : ( <div - className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`} + className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} key="title" style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, - width: !this.headerMargin ? `calc(${layout_sidebarWidthPercent || 100}% - 18px)` : (layout_sidebarWidthPercent || 100) + '%', // leave room for annotation button + width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button color: lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> <EditableView ref={this._titleRef} - contents={layout_showTitle + contents={showTitle .split(';') .map(field => field.trim()) .map(field => targetDoc[field]?.toString()) @@ -1100,27 +1136,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps fontSize={10} GetValue={() => { this.props.select(false); - return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle; + 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) { this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; } else { - Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; + Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; } } else { - var value = input.replace(new RegExp(layout_showTitle + '='), '') as string | number; - if (layout_showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (layout_showTitle.includes('Date') || layout_showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, layout_showTitle, value, true); + var value = input.replace(new RegExp(showTitle + '='), '') as string | number; + if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (showTitle.includes('Date') || showTitle === 'author') return true; + Doc.SetInPlace(targetDoc, showTitle, value, true); } return true; })} /> </div> ); - return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? ( + return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> @@ -1135,7 +1171,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps renderDoc = (style: object) => { TraceMobx(); - return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate + return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DocData]) === AclPrivate ? null : this.docContents ?? ( <div @@ -1147,8 +1183,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps opacity: this.opacity, cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', color: StrCast(this.layoutDoc.color, 'inherit'), - fontFamily: StrCast(this.Document._fontFamily, 'inherit'), - fontSize: Cast(this.Document._fontSize, 'string', null), + fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), + fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> @@ -1187,16 +1223,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; } } + @computed get highlighting() { + return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); + } + @computed get borderPath() { + return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath); + } render() { TraceMobx(); - const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); - const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath); + const highlighting = this.highlighting; + const borderPath = this.borderPath; const boxShadow = this.props.treeViewDoc || !highlighting ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` - : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + : this.boxShadow || (this.rootDoc.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', @@ -1212,15 +1254,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} - onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} - onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)} + onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} + onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} + onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)} style={{ borderRadius: this.borderRounding, pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, }}> <> - {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)} + {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} {borderPath?.jsx} </> </div> @@ -1254,8 +1296,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer); - this.rootDoc[AnimationSym] = presEffect; - this.AnimEffectTimer = setTimeout(() => (this.rootDoc[AnimationSym] = undefined), timeInMs); + this.rootDoc[Animation] = presEffect; + this.AnimEffectTimer = setTimeout(() => (this.rootDoc[Animation] = undefined), timeInMs); }; public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; @@ -1284,13 +1326,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { } // shows a stacking view collection (by default, but the user can change) of all documents linked to the source - public static showBackLinks(linkSource: Doc) { - const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; + public static showBackLinks(linkAnchor: Doc) { + const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; // prettier-ignore - DocServer.GetRefField(docId).then(docx => docx instanceof Doc && + DocServer.GetRefField(docId).then(docx => LightboxView.SetLightboxDoc( - docx || // reuse existing pivot view of documents, or else create a new collection - Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId) + (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection + Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self, self.target)') }, docId) ) ); } @@ -1326,7 +1368,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.rootDoc['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.rootDoc['link_anchor_1']) : undefined; } @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : '')); + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : '')); } @computed get linkCountView() { const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; @@ -1345,7 +1387,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); } @computed get shouldNotScale() { - return (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return this.props.shouldNotScale?.() || (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1478,8 +1520,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { // increase max auto height if document has been resized to be greater than current max () => NumCast(this.layoutDoc._height), action(height => { - const docMax = NumCast(this.layoutDoc.docMaxAutoHeight); - if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height; + const docMax = NumCast(this.layoutDoc.layout_maxAutoHeight); + if (docMax && docMax < height) this.layoutDoc.layout_maxAutoHeight = height; }) ); !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); @@ -1565,22 +1607,23 @@ ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuff else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); }); -ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { - const linkSource = Cast(linkCollection.linkSource, Doc, null); +ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); - let wid = linkSource[WidthSym](); + let wid = linkSource[Width](); + let embedding: Doc | undefined; const links = LinkManager.Links(linkSource); links.forEach(link => { const other = LinkManager.getOppositeAnchor(link, linkSource); - const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + const otherdoc = DocCast(other?.annotationOn ?? other); if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { - const embedding = Doc.MakeEmbedding(otherdoc); + embedding = Doc.MakeEmbedding(otherdoc); embedding.x = wid; embedding.y = 0; embedding._lockedPosition = false; - wid += otherdoc[WidthSym](); + wid += otherdoc[Width](); Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding); } }); + embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise return links; }); diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index 9714e1bd0..f5871db22 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -1,8 +1,9 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .equationBox-cont { - transform-origin: top left; + transform-origin: center; + background-color: #e7e7e7; > span { - width: 100%; + width: 100%; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index f17ab06e7..a77e4bdd1 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react'; import { action, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { WidthSym } from '../../../fields/Doc'; +import { Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -26,7 +26,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this.props.select(false); this._ref.current!.mathField.focus(); - this._ref.current!.mathField.select(); + this.rootDoc.text === 'x' && this._ref.current!.mathField.select(); + EquationBox.SelectOnLoad = ''; } reaction( () => StrCast(this.dataDoc.text), @@ -35,6 +36,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this._ref.current!.mathField.latex(text); } } + //{ fireImmediately: true } ); reaction( () => this.props.isSelected(), @@ -53,9 +55,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', '')); if (e.key === 'Enter') { - const nextEq = Docs.Create.EquationDocument({ + const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', { title: '# math', - text: StrCast(this.dataDoc.text), _width, _height: 25, x: NumCast(this.layoutDoc.x), @@ -67,7 +68,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (e.key === 'Tab') { const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], { - x: NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym](), + x: NumCast(this.layoutDoc.x) + this.layoutDoc[Width](), y: NumCast(this.layoutDoc.y), _width: 400, _height: 300, @@ -101,10 +102,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { width: 'fit-content', // `${100 / scale}%`, height: `${100 / scale}%`, pointerEvents: !this.props.isSelected() ? 'none' : undefined, - fontSize: StrCast(this.rootDoc._fontSize), + fontSize: StrCast(this.rootDoc._text_fontSize), }} onKeyDown={e => e.stopPropagation()}> - <EquationEditor ref={this._ref} value={this.dataDoc.text || 'x'} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> + <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> </div> ); } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 85dd779fc..4ebf22ddf 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -45,7 +45,7 @@ export interface FieldViewProps extends DocumentViewSharedProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" } @computed diff --git a/src/client/views/nodes/button/ButtonInterface.ts b/src/client/views/nodes/FontIconBox/ButtonInterface.ts index 0aa2ac8e1..0aa2ac8e1 100644 --- a/src/client/views/nodes/button/ButtonInterface.ts +++ b/src/client/views/nodes/FontIconBox/ButtonInterface.ts diff --git a/src/client/views/nodes/button/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss index 78f506e57..2ff5c651f 100644 --- a/src/client/views/nodes/button/FontIconBadge.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBadge.scss @@ -1,11 +1,12 @@ .fontIconBadge { - background: red; + background: lightgreen; width: 15px; height: 15px; top: 8px; + color: black; display: block; position: absolute; right: 5; border-radius: 50%; text-align: center; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FontIconBox/FontIconBadge.tsx b/src/client/views/nodes/FontIconBox/FontIconBadge.tsx new file mode 100644 index 000000000..b50588ce2 --- /dev/null +++ b/src/client/views/nodes/FontIconBox/FontIconBadge.tsx @@ -0,0 +1,37 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; +import './FontIconBadge.scss'; + +interface FontIconBadgeProps { + value: string | undefined; +} + +@observer +export class FontIconBadge extends React.Component<FontIconBadgeProps> { + _notifsRef = React.createRef<HTMLDivElement>(); + + // onPointerDown = (e: React.PointerEvent) => { + // setupMoveUpEvents(this, e, + // (e: PointerEvent) => { + // const dragData = new DragManager.DocumentDragData([this.props.collection!]); + // DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); + // return true; + // }, + // returnFalse, emptyFunction, false); + // } + + render() { + if (this.props.value === undefined) return null; + return ( + <div className="fontIconBadge-container" ref={this._notifsRef}> + <div + className="fontIconBadge" + style={{ display: 'initial' }} + // onPointerDown={this.onPointerDown} + > + {this.props.value} + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index f3b43501b..9d9fa26b0 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -18,7 +18,7 @@ .fontIconBox-label { color: $white; - bottom: 0; + bottom: -1; position: absolute; text-align: center; font-size: 7px; @@ -27,7 +27,7 @@ border-radius: 8px; padding: 0; width: 100%; - font-family: 'ROBOTO'; + font-family: 'system-ui'; text-transform: uppercase; font-weight: bold; transition: 0.15s; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx new file mode 100644 index 000000000..91eac675f --- /dev/null +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -0,0 +1,403 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, MultiToggle, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, NumberDropdown, NumberDropdownType, Popup, Size, 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, StrListCast } from '../../../../fields/Doc'; +import { ScriptField } from '../../../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { undoable, UndoManager } from '../../../util/UndoManager'; +import { ContextMenu } from '../../ContextMenu'; +import { DocComponent } from '../../DocComponent'; +import { EditableView } from '../../EditableView'; +import { Colors } from '../../global/globalEnums'; +import { StyleProp } from '../../StyleProvider'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { OpenWhere } from '../DocumentView'; +import { RichTextMenu } from '../formattedText/RichTextMenu'; +import './FontIconBox.scss'; +import { SelectedDocView } from '../../selectedDoc'; +import { Utils } from '../../../../Utils'; +import { FaAlignCenter, FaAlignJustify, FaAlignLeft, FaAlignRight } from 'react-icons/fa'; + +export enum ButtonType { + TextButton = 'textBtn', + MenuButton = 'menuBtn', + DropdownList = 'dropdownList', + DropdownButton = 'dropdownBtn', + ClickButton = 'clickBtn', + ToggleButton = 'toggleBtn', + ColorButton = 'colorBtn', + ToolButton = 'toolBtn', + MultiToggleButton = 'multiToggleBtn', + NumberSliderButton = 'numSliderBtn', + NumberDropdownButton = 'numDropdownBtn', + NumberInlineButton = 'numInlineBtn', + EditableText = 'editableText', +} + +export interface ButtonProps extends FieldViewProps { + type?: ButtonType; +} +@observer +export class FontIconBox extends DocComponent<ButtonProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FontIconBox, fieldKey); + } + @observable noTooltip = false; + showTemplate = (): void => { + const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); + dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); + }; + dragAsTemplate = (): void => { + this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); + }; + useAsPrototype = (): void => { + this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); + }; + + specificContextMenu = (): void => { + if (!Doc.noviceMode) { + const cm = ContextMenu.Instance; + cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' }); + cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' }); + cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' }); + } + }; + + 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)); + } + Icon = (color: string, iconFalse?: boolean) => { + let icon; + if (iconFalse) { + icon = StrCast(this.dataDoc[this.fieldKey ?? 'iconFalse'] ?? this.dataDoc.icon, 'user') as any; + if (icon) return <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + else return null; + } + icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; + const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; + return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + }; + @computed get dropdown() { + return BoolCast(this.rootDoc.dropDownOpen); + } + @computed get buttonList() { + return StrListCast(this.rootDoc.btnList); + } + @computed get type() { + return StrCast(this.rootDoc.btnType); + } + + /** + * Types of buttons in dash: + * - Main menu button (LHS) + * - Tool button + * - Expandable button (CollectionLinearView) + * - Button inside of CollectionLinearView vs. outside of CollectionLinearView + * - Action button + * - Dropdown button + * - Color button + * - Dropdown list + * - Number button + **/ + + _batch: UndoManager.Batch | undefined = undefined; + /** + * Number button + */ + @computed get numberDropdown() { + let type: NumberDropdownType; + switch (this.type) { + case ButtonType.NumberDropdownButton: + type = 'dropdown'; + break; + case ButtonType.NumberInlineButton: + type = 'input'; + break; + case ButtonType.NumberSliderButton: + default: + type = 'slider'; + break; + } + const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: value === undefined }); + 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} + numberDropdownType={type} + showPlusMinus={false} + tooltip={this.label} + type={Type.PRIM} + min={NumCast(this.rootDoc.numBtnMin, 0)} + max={NumCast(this.rootDoc.numBtnMax, 100)} + number={checkResult} + setNumber={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)} + fillWidth + /> + ); + } + + /** + * Dropdown button + */ + @computed get dropdownButton() { + 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); + return ( + <div + className={`menuButton ${this.type} ${active}`} + style={{ color: color, backgroundColor: 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 : ( + <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> + {' '} + {this.label}{' '} + </div> + )} + <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> + <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> + </div> + {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null} + </div> + ); + } + + /** + * 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); + } 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 />; + } + } else { + dropdown = false; + return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />; + } + 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 }; + }; + } + } catch (e) { + console.log(e); + } + + // 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), + val: value, + style: getStyle(value), + onClick: undoable(() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), value), + // shortcut: '#', + })); + + 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} + dropdownType={DropdownType.SELECT} + items={list} + tooltip={this.label} + fillWidth + /> + ); + } + + @computed get colorScript() { + return ScriptCast(this.rootDoc.script); + } + + /** + * Color button + */ + @computed get colorButton() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const curColor = this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent'; + const tooltip: string = StrCast(this.rootDoc.toolTip); + + return ( + <ColorPicker + setSelectedColor={value => { + const s = this.colorScript; + s && undoable(() => s.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }).result, `Set ${tooltip} to ${value}`)(); + }} + selectedColor={curColor} + type={Type.PRIM} + color={color} + icon={this.Icon(color)!} + tooltip={tooltip} + label={this.label} + /> + ); + } + @computed get multiToggleButton() { + // Determine the type of toggle button + const tooltip: string = StrCast(this.rootDoc.toolTip); + + const script = ScriptCast(this.rootDoc.onClick); + const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false; + // Colors + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const items = DocListCast(this.rootDoc.data); + return ( + <MultiToggle + tooltip={`Toggle ${tooltip}`} + type={Type.PRIM} + color={color} + 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} />, + tooltip: StrCast(item.toolTip), + val: StrCast(item.toolType), + }))} + selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)} + setSelectedVal={(val: string | number) => { + const itemDoc = items.find(item => item.toolType === val); + itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false }); + }} + /> + ); + } + + @computed get toggleButton() { + // Determine the type of toggle button + const buttonText: string = StrCast(this.rootDoc.buttonText); + const tooltip: string = StrCast(this.rootDoc.toolTip); + + const script = ScriptCast(this.rootDoc.onClick); + const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false; + // Colors + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + + return ( + <Toggle + tooltip={`Toggle ${tooltip}`} + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + toggleStatus={toggleStatus} + text={buttonText} + color={color} + icon={this.Icon(color)!} + label={this.label} + onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} + /> + ); + } + + /** + * Default + */ + @computed get defaultButton() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const tooltip: string = StrCast(this.rootDoc.toolTip); + + return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />; + } + + @computed get editableText() { + // Script for running the toggle + const script = ScriptCast(this.rootDoc.script); + // Function to run the script + const checkResult = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result; + + const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: false }).result; + + return <EditableText editing={false} setEditing={(editing: boolean) => {}} />; + + return ( + <div className="menuButton editableText"> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> + <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> + <EditableView GetValue={() => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> + </div> + </div> + ); + } + + render() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const tooltip = StrCast(this.rootDoc.toolTip); + const scriptFunc = () => ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false }); + const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label }; + // prettier-ignore + switch (this.type) { + case ButtonType.NumberDropdownButton: + case ButtonType.NumberInlineButton: + case ButtonType.NumberSliderButton: return this.numberDropdown; + case ButtonType.EditableText: return this.editableText; + case ButtonType.DropdownList: return this.dropdownListButton; + case ButtonType.ColorButton: return this.colorButton; + case ButtonType.DropdownButton: return this.dropdownButton; + case ButtonType.MultiToggleButton: return this.multiToggleButton; + 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} />; + } + return this.defaultButton; + } +} diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index db72209ab..61711417f 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -43,7 +43,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> ); } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true }); + const anchor = Docs.Create.ConfigDocument({ + // + annotationOn: this.rootDoc, + }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc); anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); @@ -77,9 +80,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { + const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc), true); + !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place - de.complete.docDragData.droppedDocuments.map(doc => Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc)); - return false; + return added; } return false; }; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 7d1d50cc7..d763753a5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -3,7 +3,8 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; -import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -77,13 +78,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor - Docs.Create.ImageanchorDocument({ + Docs.Create.ConfigDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presPanX: NumCast(this.layoutDoc._freeform_panX), presPanY: NumCast(this.layoutDoc._freeform_panY), presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), presTransition: 1000, - unrendered: true, annotationOn: this.rootDoc, }); if (anchor) { @@ -107,7 +107,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); const layoutDoc = this.layoutDoc; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[Width]() }), ({ nativeSize, width }) => { if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; @@ -135,31 +135,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { + let added: boolean | undefined = undefined; const targetIsBullseye = (ele: HTMLElement): boolean => { if (!ele) return false; if (ele === this._overlayIconRef.current) return true; return targetIsBullseye(ele.parentElement as HTMLElement); }; if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { - de.complete.docDragData.droppedDocuments.forEach( - action((drop: Doc) => { - Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); - this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - e.stopPropagation(); - }) - ); + added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { + this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); + }, true); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; const targetField = Doc.LayoutFieldKey(layoutDoc); - const targetDoc = layoutDoc[DataSym]; + const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); - e.stopPropagation(); } } + added === false && e.preventDefault(); + added !== undefined && e.stopPropagation(); + return added; } + return false; }; @undoBatch @@ -169,14 +170,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setNativeSize = action(() => { const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1); const nscale = NumCast(this.props.PanelWidth()) / scaling; - const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); - this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nh; - this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nh; - this.rootDoc._freeform_panX = nh * NumCast(this.rootDoc._freeform_panX); - this.rootDoc._freeform_panY = nh * NumCast(this.rootDoc._freeform_panY); - this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nh * NumCast(this.dataDoc._freeform_panXMax) : undefined; - this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nh * NumCast(this.dataDoc._freeform_panXMin) : undefined; + this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; + this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; + this.rootDoc._freeform_panX = nw * NumCast(this.rootDoc._freeform_panX); + this.rootDoc._freeform_panY = nw * NumCast(this.rootDoc._freeform_panY); + this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nw * NumCast(this.dataDoc._freeform_panXMax) : undefined; + this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nw * NumCast(this.dataDoc._freeform_panXMin) : undefined; this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined; this.dataDoc._freeform_panYMin = this.dataDoc._freeform_panYMin ? nw * NumCast(this.dataDoc._freeform_panYMin) : undefined; }); @@ -231,7 +231,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.freeform_panYMax = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); - cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym](); + cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width](); cropping.y = NumCast(this.rootDoc.y); this.props.addDocTab(cropping, OpenWhere.inParent); } @@ -295,7 +295,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp choosePath(url: URL) { const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; - if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf("dashblobstore") === -1) return Utils.CorsProxy(url.href); if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); @@ -303,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } considerGooglePhotosLink = () => { - const remoteUrl = this.dataDoc.googlePhotosUrl; + const remoteUrl = StrCast(this.dataDoc.googlePhotosUrl); // bcz: StrCast or URLCast??? return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />; }; @@ -387,7 +387,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (SnappingManager.GetIsDragging() && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this.props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -505,7 +505,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } })} style={{ - display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, + display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 88a82e8e6..a6712a3db 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -84,7 +84,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc; + const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); let field: Field; if (type === 'computed') { field = new ComputedField(script); @@ -213,18 +213,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> { document.addEventListener('pointerup', this.onDividerUp); }; - getFieldView = async () => { + getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document).title}`, _chromeHidden: true }); for (const row of rows) { - const field = this.createFieldView(DocCast(this.props.Document.data), row); + const field = this.createFieldView(DocCast(this.props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); } return parent; } - return rows.length ? this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(DocCast(this.props.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 2b4867321..57d36932e 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -19,7 +19,6 @@ position: relative; margin: 0; } - .keyValuePair-keyFieldMod, .keyValuePair-keyField { width: 100%; margin-left: 20px; @@ -28,9 +27,6 @@ overflow: auto; display: inline; } - .keyValuePair-keyFieldMod { - margin-left: 35px; - } } } .keyValuePair-td-value { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 509bb667e..01acdccb7 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -13,6 +13,7 @@ import { KeyValueBox } from './KeyValueBox'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import React = require('react'); +import { DocCast } from '../../../fields/Types'; // Represents one row in a key value plane @@ -56,8 +57,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { const props: FieldViewProps = { Document: this.props.doc, DataDoc: this.props.doc, - docFilters: returnEmptyFilter, - docRangeFilters: returnEmptyFilter, + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, @@ -66,7 +67,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'embed', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, @@ -87,7 +87,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { break; } protoCount++; - doc = doc.proto; + doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); const keyStyle = protoCount === 0 ? 'black' : 'blue'; @@ -104,12 +104,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { delete props.Document[props.fieldKey]; - } else delete props.Document.proto![props.fieldKey]; + } else delete DocCast(props.Document.proto)?.[props.fieldKey]; })}> X </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <div className={`keyValuePair-keyField${props.fieldKey.includes('_') ? 'Mod' : ''}`} style={{ color: keyStyle }}> + <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}> {'('.repeat(parenCount)} {props.fieldKey} {')'.repeat(parenCount)} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 32026ea9c..4439be0cd 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -74,7 +74,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp if (docDragData && missingParams?.includes((e.target as any).textContent)) { this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => (d.onDragStart ? docDragData.draggedDocuments[i] : d))); e.stopPropagation(); + return true; } + return false; }; @observable _mouseOver = false; @@ -87,8 +89,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp const params = { rotateText: null, fontSizeFactor: 1, - minimumFontSize: NumCast(this.rootDoc._minFontSize, 8), - maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000), + minimumFontSize: NumCast(this.rootDoc._label_minFontSize, 8), + maximumFontSize: NumCast(this.rootDoc._label_maxFontSize, 1000), limitingDimension: 'both', horizontalAlign: 'center', verticalAlign: 'center', @@ -130,9 +132,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp className="labelBox-mainButton" style={{ backgroundColor: this.hoverColor, - fontSize: StrCast(this.layoutDoc._fontSize), + fontSize: StrCast(this.layoutDoc._text_fontSize), color: StrCast(this.layoutDoc._color), - fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit', + fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit', letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, paddingLeft: NumCast(this.rootDoc._xPadding), diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index f38ef634c..9bcd04cf5 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -47,13 +47,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.rootDoc]); dragData.dropAction = 'embed'; - dragData.removeDropProperties = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; + dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; } else { this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; - this.rootDoc.layout_autoMoveAnchors = false; + this.rootDoc.link_autoMoveAnchors = false; } } return false; @@ -68,7 +68,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100); const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1'; - const anchorScale = !this.dataDoc[this.fieldKey + '_useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; + const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_1') ? 'link_anchor_1' diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index b5b8e660f..767f0291b 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -1,3 +1,7 @@ -.linkBox-container-interactive { +.linkBox-container-interactive { pointer-events: all; -}
\ No newline at end of file + width: 100%; +} +.linkBox-container { + width: 100%; +} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c6172ee01..d69009415 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,11 +3,12 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; -import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocCastAsync, Opt } from '../../../fields/Doc'; +import { DirectLinks, 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'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; @@ -123,7 +124,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._markerTargetDoc = linkTarget; this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } - this._toolTipText = 'link to ' + this._targetDoc?.title; if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) @@ -184,17 +184,17 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { width = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) { - return (Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]()) / this._targetDoc[HeightSym](); + if (this._targetDoc[Width]() < this._targetDoc?.[Height]()) { + return (Math.min(225, this._targetDoc[Height]()) * this._targetDoc[Width]()) / this._targetDoc[Height](); } - return Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); + return Math.min(225, NumCast(this._targetDoc?.[Width](), 225)); }; height = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[WidthSym]() > this._targetDoc?.[HeightSym]()) { - return (Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]()) / this._targetDoc[WidthSym](); + if (this._targetDoc[Width]() > this._targetDoc?.[Height]()) { + return (Math.min(225, this._targetDoc[Width]()) * this._targetDoc[Height]()) / this._targetDoc[Width](); } - return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); + return Math.min(225, NumCast(this._targetDoc?.[Height](), 225)); }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( @@ -268,8 +268,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { addDocTab={returnFalse} pinToPres={returnFalse} dontRegisterView={true} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} renderDepth={0} suppressSetHeight={true} @@ -296,7 +296,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + style={{ 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 4c3b8dabe..d4a7e18f2 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -12,6 +12,10 @@ text-overflow: ellipsis; max-width: 80%; text-align: center; + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; } } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 8c5255f80..fcbd0128d 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -8,6 +8,7 @@ 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 @@ -43,7 +44,7 @@ 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.title)); + 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); }; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index bfb0a6161..15cf22953 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -62,6 +62,12 @@ const mapOptions = { 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 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 */ @@ -473,7 +479,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); } /** @@ -544,9 +550,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.addDocument(doc, annotationKey); }; - pointerEvents = () => { - return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; - }; + 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}> @@ -583,8 +588,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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)); - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; infoWidth = () => this.props.PanelWidth() / 5; infoHeight = () => this.props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @@ -905,7 +910,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } render() { - const renderAnnotations = (docFilters?: () => string[]) => null; + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index f75b3a6a6..643aeaee0 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -3,7 +3,8 @@ import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-m import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { NumCast, StrCast } from '../../../../fields/Types'; @@ -367,7 +368,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[WidthSym](); + const fullWidth = this.layoutDoc[Width](); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { this._showSidebar = true; @@ -390,7 +391,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); } /** @@ -509,7 +510,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; pointerEvents = () => { - return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; + return this.props.isContentActive() === false ? 'none' : this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; }; @computed get annotationLayer() { return ( @@ -546,8 +547,8 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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)); - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; infoWidth = () => this.props.PanelWidth() / 5; infoHeight = () => this.props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @@ -557,7 +558,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return (window as any).Microsoft.Maps; } render() { - const renderAnnotations = (docFilters?: () => string[]) => null; + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="MapBox2" ref={this._ref}> <div diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 04cb12ade..66c47d131 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -61,7 +61,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi fieldKey="data" NativeWidth={returnZero} NativeHeight={returnZero} - docFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} setHeight={emptyFunction} isAnnotationOverlay={false} select={emptyFunction} @@ -76,7 +76,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi removeDocument={this.removeDoc} addDocument={this.addDoc} renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} + type_collection={CollectionViewType.Stacking} pointerEvents={returnAll} /> </div> diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index edeaba322..fd4c6366b 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,7 +3,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; @@ -63,7 +64,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps super(props); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; - !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); + !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); if (this.pdfUrl) { if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href))); else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf))); @@ -104,15 +105,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = this.layoutDoc[WidthSym]().toString(); - newDiv.style.height = this.layoutDoc[HeightSym]().toString(); + newDiv.style.width = this.layoutDoc[Width]().toString(); + newDiv.style.height = this.layoutDoc[Height]().toString(); this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); - const anchw = cropping[WidthSym]() * (this.props.NativeDimScaling?.() || 1); - const anchh = cropping[HeightSym]() * (this.props.NativeDimScaling?.() || 1); + const anchw = cropping[Width]() * (this.props.NativeDimScaling?.() || 1); + const anchh = cropping[Height]() * (this.props.NativeDimScaling?.() || 1); const viewScale = 1; cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); @@ -169,8 +170,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps CollectionFreeFormView.UpdateIcon( filename, docViewContent, - this.layoutDoc[WidthSym](), - this.layoutDoc[HeightSym](), + this.layoutDoc[Width](), + this.layoutDoc[Height](), this.props.PanelWidth(), this.props.PanelHeight(), NumCast(this.layoutDoc._layout_scrollTop), @@ -237,14 +238,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ele = document.createElement('div'); ele.append(this._pdfViewer.selectionContent()!); } - const docAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ + const docAnchor = () => + Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), - unrendered: true, annotationOn: this.rootDoc, }); - return anchor; - }; const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true); const anchor = annoAnchor ?? docAnchor(); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc); @@ -261,8 +259,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.dataDoc[this.props.fieldKey + '_numPages'] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72)); Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72); - this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); - !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); + this.layoutDoc._height = this.layoutDoc[Width]() / (Doc.NativeAspect(this.dataDoc) || 1); + !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); }; public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { @@ -337,7 +335,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; - onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); + onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; @@ -358,11 +356,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; - this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth; + this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); @@ -441,7 +439,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); + toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -557,11 +555,10 @@ 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.sidebarViewType))} + {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} </div> ); } - isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath())); @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; @@ -594,7 +591,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pdf={this._pdf} focus={this.focus} url={this.pdfUrl!.url.pathname} - isContentActive={this.isPdfContentActive} anchorMenuClick={this.anchorMenuClick} loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx index 2c06282ed..cd1ff17dd 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx @@ -1,11 +1,11 @@ -// import ArrowLeftIcon from '@mui/icons-material/ArrowLeft'; -// import ArrowRightIcon from '@mui/icons-material/ArrowRight'; -// import PauseIcon from '@mui/icons-material/Pause'; -// import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -// import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; -// import ReplayIcon from '@mui/icons-material/Replay'; -// import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material'; -// import Typography from '@mui/material/Typography'; +import ArrowLeftIcon from '@mui/icons-material/ArrowLeft'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import PauseIcon from '@mui/icons-material/Pause'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; +import ReplayIcon from '@mui/icons-material/Replay'; +import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material'; +import Typography from '@mui/material/Typography'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NumListCast } from '../../../../fields/Doc'; @@ -928,12 +928,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP <Stack direction="row" spacing={1}> {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}> - {/* <PlayArrowIcon /> */} + <PlayArrowIcon /> </IconButton> )} {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}> - {/* <PauseIcon /> */} + <PauseIcon /> </IconButton> )} {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( @@ -1180,7 +1180,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; }} disabled={this.dataDoc.tutorial_stepNumber == 0}> - {/* <ArrowLeftIcon /> */} + <ArrowLeftIcon /> </IconButton> <div> <h3> @@ -1199,7 +1199,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; }} disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}> - {/* <ArrowRightIcon /> */} + <ArrowRightIcon /> </IconButton> </div> <div> diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss deleted file mode 100644 index b5f90aa1e..000000000 --- a/src/client/views/nodes/QueryBox.scss +++ /dev/null @@ -1,5 +0,0 @@ -.queryBox, .queryBox-dragging { - width: 100%; - height: 100%; - position: absolute; -}
\ No newline at end of file diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx deleted file mode 100644 index 1b6056be6..000000000 --- a/src/client/views/nodes/QueryBox.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// import React = require("react"); -// import { IReactionDisposer } from "mobx"; -// import { observer } from "mobx-react"; -// import { documentSchema } from "../../../new_fields/documentSchemas"; -// import { Id } from '../../../new_fields/FieldSymbols'; -// import { makeInterface, listSpec } from "../../../new_fields/Schema"; -// import { StrCast, Cast } from "../../../new_fields/Types"; -// import { ViewBoxAnnotatableComponent } from '../DocComponent'; -// import { SearchBox } from "../search/SearchBox"; -// import { FieldView, FieldViewProps } from './FieldView'; -// import "./QueryBox.scss"; -// import { List } from "../../../new_fields/List"; -// import { SnappingManager } from "../../util/SnappingManager"; - -// type QueryDocument = makeInterface<[typeof documentSchema]>; -// const QueryDocument = makeInterface(documentSchema); - -// @observer -// export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) { -// public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); } -// _docListChangedReaction: IReactionDisposer | undefined; -// componentDidMount() { -// } - -// componentWillUnmount() { -// this._docListChangedReaction?.(); -// } - -// render() { -// const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging"; -// return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > - -// <SearchBox Document={this.props.Document} /> -// </div >; -// } -// } - -// //<SearchBox id={this.props.Document[Id]} sideBar={side} Document={this.props.Document} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQuery={this.dataDoc.filterQuery} /> diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 80b12b96e..04f11a5df 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -11,6 +11,7 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { Presentation } from '../../../util/TrackMovements'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; +import { DocCast } from '../../../../fields/Types'; @observer export class RecordingBox extends ViewBoxBaseComponent() { @@ -57,7 +58,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { render() { return ( <div className="recordingBox" ref={this._ref}> - {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />} + {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />} </div> ); } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 424ebc384..51eb774e2 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -67,7 +67,7 @@ export function RecordingView(props: IRecordingViewProps) { const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({file})))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 6fb5ea7b3..1e9b64a0b 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -1,6 +1,5 @@ .screenshotBox { transform-origin: top left; - background: white; color: black; // .screenshotBox-viewer { // opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger @@ -12,46 +11,39 @@ #CANCAN { canvas { - width:100% !important; + width: 100% !important; height: 100% !important; } } -.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen { +.screenshotBox-content, +.screenshotBox-content-interactive, +.screenshotBox-cont-fullScreen { width: 100%; z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt position: absolute; } -.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen { - height: Auto; +.screenshotBox-content, +.screenshotBox-content-interactive, +.screenshotBox-content-fullScreen { + height: Auto; } .screenshotBox-uiButtons { - background:dimgray; - border: orange solid 1px; position: absolute; right: 25; top: 0; - width:25; + width: 22; height: 25; - .screenshotBox-snapshot{ - color : white; - top :0px; - right : 5px; - position: absolute; - background-color:rgba(50, 50, 50, 0.2); - transform-origin: left top; - pointer-events:all; - } - .screenshotBox-recorder{ - color : white; - top :0px; + .screenshotBox-recorder { + color: white; + top: 4px; left: 5px; position: absolute; - background-color:rgba(50, 50, 50, 0.2); + background-color: rgba(50, 50, 50, 0.2); transform-origin: left top; - pointer-events:all; + pointer-events: all; } } diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 7bf765042..271ff3cf8 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -5,10 +5,11 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; import { DateField } from '../../../fields/DateField'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast } from '../../../fields/Types'; import { AudioField, VideoField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; @@ -24,6 +25,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import './ScreenshotBox.scss'; import { VideoBox } from './VideoBox'; +import { SettingsManager } from '../../util/SettingsManager'; + declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has } @@ -140,7 +143,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl if (!nativeWidth || !nativeHeight) { if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; + this.layoutDoc._height = (this.layoutDoc[Width]() || 0) / aspect; } }; @@ -222,7 +225,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const aud_chunks: any = []; this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file }))); if (!(result instanceof Error)) { this.dataDoc[this.props.fieldKey + '-audio'] = new AudioField(result.accessPaths.agnostic.client); } @@ -233,9 +236,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this._videoRec.onstart = () => (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) => { - console.log('screenshotbox: upload'); const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); - const [{ result }] = await Networking.UploadFilesToServer(file); + const [{ result }] = await Networking.UploadFilesToServer({ file }); this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox @@ -277,7 +279,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); this.dataDoc[this.fieldKey + '-dictation'] = dictationText; }; - videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth(); + videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[Height]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[Width]())) * this.props.PanelWidth(); formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { TraceMobx(); @@ -311,11 +313,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </> </CollectionFreeFormView> </div> - <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}> + <div style={{ background: SettingsManager.Instance.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : ( <FormattedTextBox {...this.props} - Document={this.dataDoc[this.fieldKey + '-dictation']} + Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])} fieldKey={'text'} PanelHeight={this.formattedPanelHeight} select={emptyFunction} @@ -333,8 +335,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div> </div> {!this.props.isSelected() ? null : ( - <div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}> + <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}> <FontAwesomeIcon icon="file" size="lg" /> </div> </div> diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 37fda14fc..7c8a1849e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -270,8 +270,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // sets field of the corresponding field key (param name) to be dropped document @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { - Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true); - e.stopPropagation(); + if (de.complete.docDragData) { + de.complete.docDragData.droppedDocuments.forEach(doc => Doc.SetInPlace(this.rootDoc, fieldKey, doc, true)); + e.stopPropagation(); + return true; + } + return false; }; // deletes a param from all areas in which it is stored @@ -606,7 +610,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable ' ': { dataProvider: (token: any) => this.handleToken(token), component: (blob: any) => { - console.log('Blob', blob); return this.renderFuncListElement(blob.entity); }, output: (item: any, trigger: any) => { @@ -617,7 +620,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable '.': { dataProvider: (token: any) => this.handleToken(token), component: (blob: any) => { - console.log('Blob', blob); return this.renderFuncListElement(blob.entity); }, output: (item: any, trigger: any) => { diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index b96977f32..430b20eb5 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -12,50 +12,56 @@ import { FieldView, FieldViewProps } from './FieldView'; import { Handle, Tick, TooltipRail, Track } from './SliderBox-components'; import './SliderBox.scss'; - @observer export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SliderBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(SliderBox, fieldKey); + } - get minThumbKey() { return this.fieldKey + "-minThumb"; } - get maxThumbKey() { return this.fieldKey + "-maxThumb"; } - get minKey() { return this.fieldKey + "-min"; } - get maxKey() { return this.fieldKey + "-max"; } + get minThumbKey() { + return this.fieldKey + '-minThumb'; + } + get maxThumbKey() { + return this.fieldKey + '-maxThumb'; + } + get minKey() { + return this.fieldKey + '-min'; + } + get maxKey() { + return this.fieldKey + '-max'; + } specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Edit Thumb Change Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Thumb Change ...", this.props.Document, "onThumbChange", obj.x, obj.y) }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - } - onChange = (values: readonly number[]) => runInAction(() => { - this.dataDoc[this.minThumbKey] = values[0]; - this.dataDoc[this.maxThumbKey] = values[1]; - ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ - self: this.rootDoc, - scriptContext: this.props.scriptContext, range: values, this: this.layoutDoc + funcs.push({ description: 'Edit Thumb Change Script', icon: 'edit', event: (obj: any) => ScriptBox.EditButtonScript('On Thumb Change ...', this.props.Document, 'onThumbChange', obj.x, obj.y) }); + ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); + }; + onChange = (values: readonly number[]) => + runInAction(() => { + this.dataDoc[this.minThumbKey] = values[0]; + this.dataDoc[this.maxThumbKey] = values[1]; + ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ + self: this.rootDoc, + scriptContext: this.props.scriptContext, + range: values, + this: this.layoutDoc, + }); }); - }) render() { const domain = [NumCast(this.layoutDoc[this.minKey]), NumCast(this.layoutDoc[this.maxKey])]; const defaultValues = [NumCast(this.dataDoc[this.minThumbKey]), NumCast(this.dataDoc[this.maxThumbKey])]; - return domain[1] <= domain[0] ? (null) : ( - <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} - style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> - <div className="sliderBox-mainButton" - onContextMenu={this.specificContextMenu} style={{ + return domain[1] <= domain[0] ? null : ( + <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> + <div + className="sliderBox-mainButton" + onContextMenu={this.specificContextMenu} + style={{ background: StrCast(this.layoutDoc.backgroundColor), - color: StrCast(this.layoutDoc.color, "black"), - fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) - }} > - <Slider - mode={2} - step={Math.min(1, .1 * (domain[1] - domain[0]))} - domain={domain} - rootStyle={{ position: "relative", width: "100%" }} - onChange={this.onChange} - values={defaultValues} - > - + color: StrCast(this.layoutDoc.color, 'black'), + fontSize: StrCast(this.layoutDoc._text_fontSize), + letterSpacing: StrCast(this.layoutDoc.letterSpacing), + }}> + <Slider mode={2} step={Math.min(1, 0.1 * (domain[1] - domain[0]))} domain={domain} rootStyle={{ position: 'relative', width: '100%' }} onChange={this.onChange} values={defaultValues}> <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> <Handles> {({ handles, activeHandleID, getHandleProps }) => ( @@ -64,13 +70,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { const value = i === 0 ? defaultValues[0] : defaultValues[1]; return ( <div title={String(value)}> - <Handle - key={handle.id} - handle={handle} - domain={domain} - isActive={handle.id === activeHandleID} - getHandleProps={getHandleProps} - /> + <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> </div> ); })} @@ -81,13 +81,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { {({ tracks, getTrackProps }) => ( <div className="slider-tracks"> {tracks.map(({ id, source, target }) => ( - <Track - key={id} - source={source} - target={target} - disabled={false} - getTrackProps={getTrackProps} - /> + <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> ))} </div> )} @@ -95,13 +89,8 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { <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()} - /> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> ))} </div> )} @@ -111,4 +100,4 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index e00cb8618..1f52c2d92 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,7 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import { basename } from 'path'; -import { Doc, HeightSym, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -14,6 +15,7 @@ import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; +import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ReplayMovements } from '../../util/ReplayMovements'; import { SelectionManager } from '../../util/SelectionManager'; @@ -33,8 +35,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; -import { ScriptField } from '../../../fields/ScriptField'; -import { FollowLinkScript } from '../../util/LinkFollower'; const path = require('path'); /** @@ -111,7 +111,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // returns the presentation data if it exists, null otherwise @computed get presentation() { const data = this.dataDoc[this.fieldKey + '-presentation']; - return data ? JSON.parse(data) : null; + return data ? JSON.parse(StrCast(data)) : null; } @computed private get timeline() { @@ -354,8 +354,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp updateIcon = () => { const makeIcon = (returnedfilename: string) => { this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc.icon_nativeWidth = this.layoutDoc[WidthSym](); - this.dataDoc.icon_nativeHeight = this.layoutDoc[HeightSym](); + this.dataDoc.icon_nativeWidth = this.layoutDoc[Width](); + this.dataDoc.icon_nativeHeight = this.layoutDoc[Height](); }; this.Snapshot(undefined, undefined, makeIcon); }; @@ -465,7 +465,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp { fireImmediately: true } ); - (!this.dataDoc[this.fieldKey + '_thumbnails'] || this.dataDoc[this.fieldKey + '_thumbnails'].length != VideoBox.numThumbnails) && this.getVideoThumbnails(); + (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails(); } }; @@ -870,6 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false, false ); } @@ -912,7 +913,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100; marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; - timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; + timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`]; // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { @@ -1026,13 +1027,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.videoCrop = true; croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode; croppingProto.freeform_scale = viewScale; - croppingProto.freeform_scaleMin = viewScale; + croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_ = 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' }); } @@ -1081,8 +1082,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp PanelHeight={this.panelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} - docFilters={this.timelineDocFilter} + childFilters={this.timelineDocFilter} select={emptyFunction} + focus={emptyFunction} NativeDimScaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 3da5d8f17..f5df42161 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,16 +2,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, removeStyleSheetRule, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -37,7 +39,6 @@ import { LinkDocPreview } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; import React = require('react'); -import { RefField } from '../../../fields/RefField'; const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); @@ -84,7 +85,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._url ? WebBox.urlHash(this._url) + '' : ''; } @computed get scrollHeight() { - return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight); + return Math.max(this.layoutDoc[Height](), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); @@ -124,11 +125,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._searchRef.current?.setRangeText(searchString); }); } - if (clear) { - this._iframe?.contentWindow?.getSelection()?.empty(); - } - if (searchString) { - (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); + try { + if (clear) { + this._iframe?.contentWindow?.getSelection()?.empty(); + } + if (searchString) { + (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); + } + } catch (e) { + console.log("WebBox search error", e) } return true; }; @@ -214,7 +219,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); - this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; + this.layoutDoc._height = this.layoutDoc[Width]() / youtubeaspect; } } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { @@ -281,7 +286,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps focus = (anchor: Doc, options: DocFocusOptions) => { if (anchor !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight)); + const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[Height](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[Height](), this._scrollHeight)); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { const focusTime = options.zoomTime ?? 500; @@ -296,8 +301,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action getView = (doc: Doc) => { + if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.())); if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify(); - if (this._url && WebCast(doc.presData).url.href !== this._url) this.setData(WebCast(doc.presData).url.href); + const webUrl = WebCast(doc.presData)?.url; + if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; @@ -320,10 +327,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations, false) ?? - Docs.Create.WebanchorDocument({ + Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), y: NumCast(this.layoutDoc._layout_scrollTop), - unrendered: true, annotationOn: this.rootDoc, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); @@ -459,7 +465,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); if (this._scrollHeight) { this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight); - this.layoutDoc.height = Math.min(this.layoutDoc[HeightSym](), (this.layoutDoc[WidthSym]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); + this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); } }; initHeights(); @@ -472,13 +478,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 'click', undoBatch( action((e: MouseEvent) => { - const batch = UndoManager.StartBatch('webclick'); let href = ''; for (let ele = e.target as any; ele; ele = ele.parentElement) { href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } const origin = this.webField?.origin; if (href && origin) { + const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); setTimeout(() => { this.setData(href.replace(Utils.prepend(''), origin)); @@ -509,7 +515,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._innerCollectionView) { this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY); const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop); - this.layoutDoc.panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale); + this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale); } e.preventDefault(); } @@ -744,6 +750,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} 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`} @@ -782,7 +789,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); - onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); + onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; @@ -818,9 +825,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps toggleSidebar = action((preview: boolean = false) => { var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); if (!nativeWidth) { - const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym](); + const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[Width](); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[Height]() / this.Document[Width]()) * defaultNativeWidth); nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); } const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; @@ -828,11 +835,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar; - this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth; + this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; if (!this.layoutDoc._layout_showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '_nativeWidth'] = undefined; } else { @@ -897,7 +904,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (docFilters: () => string[]) => ( + const renderAnnotations = (childFilters: () => string[]) => ( <CollectionFreeFormView {...this.props} setContentView={this.setInnerContent} @@ -915,8 +922,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ScreenToLocalTransform={this.scrollXf} NativeDimScaling={returnOne} focus={this.focus} - dropAction="embed" - docFilters={docFilters} + childFilters={childFilters} select={emptyFunction} isAnyChildContentActive={returnFalse} bringToFront={emptyFunction} @@ -924,7 +930,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={this.addDocument} + addDocument={this.addDocumentWrapper} childPointerEvents={this.props.isContentActive() ? 'all' : undefined} pointerEvents={this.annotationPointerEvents} /> @@ -992,16 +998,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return 'none'; + if (this.inlineTextAnnotations.includes(doc)) return 'none'; } return this.props.styleProvider?.(doc, props, property); }; - pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); - annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); + pointerEvents = () => + !this._draggingSidebar && this.props.isContentActive() && !MarqueeOptionsMenu.Instance?.isShown() + ? 'all' // + : 'none'; + annotationPointerEvents = () => (this.props.isContentActive() && (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); render() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); @@ -1010,7 +1019,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="webBox" ref={this._mainCont} - style={{ pointerEvents: this.pointerEvents(), position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}> + style={{ + pointerEvents: this.pointerEvents(), // + position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, + display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, + }}> <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> <div className="webBox-container" diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx deleted file mode 100644 index 3b5aac221..000000000 --- a/src/client/views/nodes/button/FontIconBadge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { observer } from "mobx-react"; -import * as React from "react"; -import { AclPrivate, Doc, DocListCast } from "../../../../fields/Doc"; -import { GetEffectiveAcl } from "../../../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; -import { DragManager } from "../../../util/DragManager"; -import "./FontIconBadge.scss"; - -interface FontIconBadgeProps { - value: string | undefined; -} - -@observer -export class FontIconBadge extends React.Component<FontIconBadgeProps> { - _notifsRef = React.createRef<HTMLDivElement>(); - - // onPointerDown = (e: React.PointerEvent) => { - // setupMoveUpEvents(this, e, - // (e: PointerEvent) => { - // const dragData = new DragManager.DocumentDragData([this.props.collection!]); - // DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); - // return true; - // }, - // returnFalse, emptyFunction, false); - // } - - render() { - if (this.props.value === undefined) return (null); - return <div className="fontIconBadge-container" ref={this._notifsRef}> - <div className="fontIconBadge" style={{ "display": "initial" }} - // onPointerDown={this.onPointerDown} - > - {this.props.value} - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx deleted file mode 100644 index 5e615f2c1..000000000 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ /dev/null @@ -1,960 +0,0 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { ColorState, SketchPicker } from 'react-color'; -import { Doc, HeightSym, StrListCast, WidthSym } from '../../../../fields/Doc'; -import { InkTool } from '../../../../fields/InkField'; -import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { WebField } from '../../../../fields/URLField'; -import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, Utils } from '../../../../Utils'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { LinkManager } from '../../../util/LinkManager'; -import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; -import { ContextMenu } from '../../ContextMenu'; -import { DocComponent } from '../../DocComponent'; -import { EditableView } from '../../EditableView'; -import { GestureOverlay } from '../../GestureOverlay'; -import { Colors } from '../../global/globalEnums'; -import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../../InkingStroke'; -import { InkTranscription } from '../../InkTranscription'; -import { StyleProp } from '../../StyleProvider'; -import { FieldView, FieldViewProps } from '.././FieldView'; -import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView'; -import { OpenWhere } from '../DocumentView'; -import { RichTextMenu } from '../formattedText/RichTextMenu'; -import { WebBox } from '../WebBox'; -import { FontIconBadge } from './FontIconBadge'; -import './FontIconBox.scss'; - -export enum ButtonType { - TextButton = 'textBtn', - MenuButton = 'menuBtn', - DropdownList = 'drpdownList', - DropdownButton = 'drpdownBtn', - ClickButton = 'clickBtn', - DoubleButton = 'dblBtn', - ToggleButton = 'tglBtn', - ColorButton = 'colorBtn', - ToolButton = 'toolBtn', - NumberSliderButton = 'numSliderBtn', - NumberDropdownButton = 'numDropdownBtn', - NumberInlineButton = 'numInlineBtn', - EditableText = 'editableText', -} - -export interface ButtonProps extends FieldViewProps { - type?: ButtonType; -} -@observer -export class FontIconBox extends DocComponent<ButtonProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(FontIconBox, fieldKey); - } - @observable noTooltip = false; - showTemplate = (): void => { - const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); - }; - dragAsTemplate = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - }; - useAsPrototype = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); - }; - - specificContextMenu = (): void => { - if (!Doc.noviceMode) { - const cm = ContextMenu.Instance; - cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' }); - } - }; - - 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.label, StrCast(this.rootDoc.title)); - } - Icon = (color: string) => { - const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; - const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; - return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; - }; - @computed get dropdown() { - return BoolCast(this.rootDoc.dropDownOpen); - } - @computed get buttonList() { - return StrListCast(this.rootDoc.btnList); - } - @computed get type() { - return StrCast(this.rootDoc.btnType); - } - - /** - * Types of buttons in dash: - * - Main menu button (LHS) - * - Tool button - * - Expandable button (CollectionLinearView) - * - Button inside of CollectionLinearView vs. outside of CollectionLinearView - * - Action button - * - Dropdown button - * - Color button - * - Dropdown list - * - Number button - **/ - - _batch: UndoManager.Batch | undefined = undefined; - /** - * Number button - */ - @computed get numberSliderButton() { - const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); - - // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); - - const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; - - const dropdown = ( - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}> - <input - type="range" - step="1" - min={NumCast(this.rootDoc.numBtnMin, 0)} - max={NumCast(this.rootDoc.numBtnMax, 100)} - value={checkResult} - className="menu-slider" - onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))} - onPointerUp={() => this._batch?.end()} - onChange={e => { - e.stopPropagation(); - setValue(Number(e.target.value)); - }} - /> - </div> - ); - return ( - <div - className="menuButton numBtn slider" - onPointerDown={e => e.stopPropagation()} - onClick={action(() => { - this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; - this.noTooltip = this.rootDoc.dropDownOpen; - Doc.UnBrushAllDocs(); - })}> - {checkResult} - {label} - {this.rootDoc.dropDownOpen ? dropdown : null} - </div> - ); - } - /** - * Number button - */ - @computed get numberDropdownButton() { - const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); - - // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); - - const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; - - const items: number[] = []; - for (let i = 0; i < 100; i++) { - if (i % 2 === 0) { - items.push(i); - } - } - const list = items.map(value => { - return ( - <div - className="list-item" - key={`${value}`} - style={{ - backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined, - }} - onClick={() => setValue(value)}> - {value} - </div> - ); - }); - return ( - <div className="menuButton numBtn list"> - <div className="button" onClick={action(e => setValue(Number(checkResult) - 1))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} /> - </div> - <div - className={`button ${'number'}`} - onPointerDown={e => { - e.stopPropagation(); - e.preventDefault(); - }} - onClick={action(() => { - this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; - this.noTooltip = this.rootDoc.dropDownOpen; - Doc.UnBrushAllDocs(); - })}> - <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} /> - </div> - <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} /> - </div> - - {this.rootDoc.dropDownOpen ? ( - <div> - <div className="menuButton-dropdownList" style={{ left: '25%' }}> - {list} - </div> - <div - className="dropbox-background" - onClick={action(e => { - e.stopPropagation(); - this.rootDoc.dropDownOpen = false; - this.noTooltip = false; - Doc.UnBrushAllDocs(); - })} - /> - </div> - ) : null} - </div> - ); - } - /** - * Number button - */ - @computed get numberInlineButton() { - return <div />; - } - - /** - * Dropdown button - */ - @computed get dropdownButton() { - 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); - return ( - <div - className={`menuButton ${this.type} ${active}`} - style={{ color: color, backgroundColor: 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 : ( - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> - {' '} - {this.label}{' '} - </div> - )} - <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> - <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> - </div> - {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null} - </div> - ); - } - - /** - * 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 icon: IconProp = 'caret-down'; - try { - if (script?.script.originalScript.startsWith('setView')) { - const selected = SelectionManager.Docs().lastElement(); - if (selected) { - if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._viewType); - } else { - dropdown = false; - text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type); - icon = Doc.toIcon(selected); - } - } else { - dropdown = false; - icon = 'globe-asia'; - text = 'User Default'; - } - noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; - } else text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - } catch (e) { - console.log(e); - } - - // Get items to place into the list - const list = this.buttonList - .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) - .map(value => ( - <div - className="list-item" - key={`${value}`} - style={{ - fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, - backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, - }} - onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}> - {value[0].toUpperCase() + value.slice(1)} - </div> - )); - - const label = - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ bottom: 0, position: 'absolute', color: color, backgroundColor: backgroundColor }}> - {this.label} - </div> - ); - - return ( - <div - className={`menuButton ${this.type} ${active}`} - style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }} - onClick={ - dropdown - ? action(() => { - this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; - this.noTooltip = this.rootDoc.dropDownOpen; - Doc.UnBrushAllDocs(); - }) - : undefined - }> - {dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />} - <div className="menuButton-dropdown-header">{text && text[0].toUpperCase() + text.slice(1)}</div> - {label} - {!dropdown ? null : ( - <div className="menuButton-dropDown"> - <FontAwesomeIcon icon={icon} color={color} size="sm" /> - </div> - )} - {this.rootDoc.dropDownOpen ? ( - <div> - <div className="menuButton-dropdownList" style={{ left: 0 }}> - {list} - </div> - <div - className="dropbox-background" - onClick={action(e => { - e.stopPropagation(); - this.rootDoc.dropDownOpen = false; - this.noTooltip = false; - Doc.UnBrushAllDocs(); - })} - /> - </div> - ) : null} - </div> - ); - } - - @observable colorPickerClosed: boolean = true; - @computed get colorScript() { - return ScriptCast(this.rootDoc.script); - } - - colorPicker = (curColor: string) => { - const change = (value: ColorState, ev: MouseEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - const s = this.colorScript; - s && undoBatch(() => s.script.run({ self: this.rootDoc, value: Utils.colorString(value), _readOnly_: false }).result)(); - }; - const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']; - return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />; - }; - /** - * Color button - */ - @computed get colorButton() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent'; - - const label = - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor }}> - {this.label} - </div> - ); - - return ( - <div - className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`} - style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={action(e => { - this.colorPickerClosed = !this.colorPickerClosed; - this.noTooltip = !this.colorPickerClosed; - setTimeout(() => Doc.UnBrushAllDocs()); - e.stopPropagation(); - })} - onPointerDown={e => e.stopPropagation()}> - {this.Icon(color)} - <div className="colorButton-color" style={{ backgroundColor: curColor }} /> - {label} - {/* {dropdownCaret} */} - {this.colorPickerClosed ? null : ( - <div> - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> - {this.colorPicker(curColor)} - </div> - <div - className="dropbox-background" - onPointerDown={action(e => { - e.preventDefault(); - e.stopPropagation(); - this.colorPickerClosed = true; - this.noTooltip = false; - Doc.UnBrushAllDocs(); - })} - /> - </div> - )} - </div> - ); - } - - @computed get toggleButton() { - // Determine the type of toggle button - const switchToggle: boolean = BoolCast(this.rootDoc.switchToggle); - const buttonText: string = StrCast(this.rootDoc.buttonText); - // Colors - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - - // Button label - const label = - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor }}> - {this.label} - </div> - ); - - if (switchToggle) { - return ( - <div className={`menuButton ${this.type} ${'switch'}`}> - {buttonText ? buttonText : null} - <label className="switch"> - <input type="checkbox" checked={backgroundColor === Colors.MEDIUM_BLUE} /> - <span className="slider round" /> - </label> - </div> - ); - } else { - return ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}> - {this.Icon(color)} - {label} - </div> - ); - } - } - - /** - * Default - */ - @computed get defaultButton() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - return ( - <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> - <div className="menuButton-wrap"> - {this.Icon(color)} - {!this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> - {' '} - {this.label}{' '} - </div> - )} - </div> - </div> - ); - } - - @computed get editableText() { - // Script for running the toggle - const script = ScriptCast(this.rootDoc.script); - // Function to run the script - const checkResult = script?.script.run({ value: '', _readOnly_: true }).result; - - const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ value, _readOnly_: false }).result; - return ( - <div className="menuButton editableText"> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> - <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> - </div> - </div> - ); - } - - render() { - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const label = (noBackground: boolean = false) => - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}> - {this.label} - </div> - ); - // TODO:glr Add label of button type - let button: JSX.Element = this.defaultButton; - - // prettier-ignore - switch (this.type) { - case ButtonType.EditableText: return this.editableText; - case ButtonType.DropdownList: button = this.dropdownListButton; break; - case ButtonType.ColorButton: button = this.colorButton; break; - case ButtonType.NumberDropdownButton: button = this.numberDropdownButton; break; - case ButtonType.NumberInlineButton: button = this.numberInlineButton; break; - case ButtonType.NumberSliderButton: button = this.numberSliderButton; break; - case ButtonType.DropdownButton: button = this.dropdownButton; break; - case ButtonType.ToggleButton: button = this.toggleButton; break; - case ButtonType.TextButton: - // Script for checking the outcome of the toggle - const script = ScriptCast(this.rootDoc.script); - const checkResult = script?.script.run({ _readOnly_: true }).result; - button = ( - <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> - {this.Icon(color)} - {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null} - {label()} - </div> - ); - break; - case ButtonType.ClickButton: - case ButtonType.ToolButton: button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}> - {this.Icon(color)} - {label()} - </div> - ); - break; - case ButtonType.MenuButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> - {this.Icon(color)} - {label(true)} - <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} /> - </div> - ); - break; - } - - return !this.layoutDoc.toolTip || this.noTooltip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>; - } -} - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setView(view: string) { - const selected = SelectionManager.Docs().lastElement(); - selected ? (selected._viewType = view) : console.log('[FontIconBox.tsx] changeView failed'); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { - const selectedViews = SelectionManager.Views(); - if (Doc.ActiveTool !== InkTool.None) { - if (checkResult) { - return ActiveFillColor(); - } - SetActiveFillColor(color ?? 'transparent'); - } else if (selectedViews.length) { - if (checkResult) { - const selView = selectedViews.lastElement(); - const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; - const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values - const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed - return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent'; - } - selectedViews.forEach(dv => { - const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; - const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values - const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed - if (contentFrameNumber !== undefined) { - CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color }); - } else { - dv.rootDoc['_' + fieldKey] = color; - } - }); - } else { - const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : []; - if (checkResult) { - return selected.lastElement()?._backgroundColor ?? 'transparent'; - } - selected.forEach(doc => (doc._backgroundColor = color)); - } -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { - if (checkResult) { - return Doc.SharingDoc().userColor; - } - Doc.SharingDoc().userColor = undefined; - Doc.GetProto(Doc.SharingDoc()).userColor = color; - Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'creationDate'); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { - const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - if (checkResult) { - if (NumCast(selected?.Document.z) >= 1) return Colors.MEDIUM_BLUE; - return 'transparent'; - } - selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); -}); - -ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - // prettier-ignore - const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ - ['grid', { - undo: false, - checkResult: (doc:Doc) => doc._freeform_backgroundGrid, - setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, - }], - ['snaplines', { - undo: false, - checkResult: (doc:Doc) => doc._freeform_snapLines, - setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines, - }], - ['viewAll', { - undo: false, - checkResult: (doc:Doc) => doc._freeform_fitContentsToBox, - setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, - }], - ['clusters', { - undo: false, - checkResult: (doc:Doc) => doc._freeform_useClusters, - setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters, - }], - ['arrange', { - undo: true, - checkResult: (doc:Doc) => doc._autoArrange, - setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, - }], - ]); - - if (checkResult) { - return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent'; - } - const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} }; - SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); - setTimeout(() => batch.end(), 100); -}); -ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - const selected = SelectionManager.Docs().lastElement(); - // prettier-ignore - const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void;}> = new Map([ - ['font', { - checkResult: () => RichTextMenu.Instance?.fontFamily, - setDoc: () => value && RichTextMenu.Instance.setFontFamily(value), - }], - ['highlight', { - checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight, - setDoc: () => value && RichTextMenu.Instance.setHighlight(value), - }], - ['fontColor', { - checkResult: () => RichTextMenu.Instance?.fontColor, - setDoc: () => value && RichTextMenu.Instance.setColor(value), - }], - ['fontSize', { - checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), - setDoc: () => { - if (typeof value === 'number') value = value.toString(); - if (value && Number(value).toString() === value) value += 'px'; - RichTextMenu.Instance.setFontSize(value); - }, - }], - ]); - - if (checkResult) { - return map.get(attr)?.checkResult(); - } - map.get(attr)?.setDoc?.(); -}); - -type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal'; -type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }]; -ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) { - const textView = RichTextMenu.Instance?.TextView; - const editorView = textView?.EditorView; - // prettier-ignore - const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) => - [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false), - toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]); - // prettier-ignore - const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => - [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false), - 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)) }], - ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), - toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], - ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}], - ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}], - ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false), - toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]] - - const map = new Map(attrs.concat(alignments).concat(listings)); - if (checkResult) return map.get(charStyle)?.checkResult() ? Colors.MEDIUM_BLUE : 'transparent'; - map.get(charStyle)?.toggle(); -}); - -export function checkInksToGroup() { - // console.log("getting here to inks group"); - if (Doc.ActiveTool === InkTool.Write) { - 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 - // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other - const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => { - // console.log(inkDoc.x, inkDoc.y); - }); - }); - } -} - -export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { - // TODO nda - if document being added to is a inkGrouping then we can just add to that group - if (Doc.ActiveTool === InkTool.Write) { - 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; - // loop through selected an get the bound - const bounds: { x: number; y: number; width?: number; height?: number }[] = []; - - selected.map( - action(d => { - const x = NumCast(d.x); - const y = NumCast(d.y); - const width = d[WidthSym](); - const height = d[HeightSym](); - bounds.push({ x, y, width, height }); - }) - ); - - const aggregBounds = aggregateBounds(bounds, 0, 0); - const marqViewRef = ffView._marqueeViewRef.current; - - // set the vals for bounds in marqueeView - if (marqViewRef) { - marqViewRef._downX = aggregBounds.x; - marqViewRef._downY = aggregBounds.y; - marqViewRef._lastX = aggregBounds.r; - marqViewRef._lastY = aggregBounds.b; - } - - selected.map( - action(d => { - const dx = NumCast(d.x); - const dy = NumCast(d.y); - delete d.x; - delete d.y; - delete d.activeFrame; - delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - // calculate pos based on bounds - if (marqViewRef?.Bounds) { - d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; - d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; - } - return d; - }) - ); - ffView.props.removeDocument?.(selected); - // TODO: nda - this is the code to actually get a new grouped collection - const newCollection = marqViewRef?.getCollection(selected, undefined, true); - if (newCollection) { - newCollection.height = newCollection[HeightSym](); - newCollection.width = newCollection[WidthSym](); - } - - // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs - newCollection && ffView.props.addDocument?.(newCollection); - // TODO: nda - will probably need to go through and only remove the unprocessed selected docs - ffView.unprocessedDocs = []; - - InkTranscription.Instance.transcribeInk(newCollection, selected, false); - }); - } - CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); -} - -function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) { - InkTranscription.Instance?.createInkGroup(); - if (checkResult) { - return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool - ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures) - ? Colors.MEDIUM_BLUE - : Colors.MEDIUM_BLUE_ALT - : 'transparent'; - } - runInAction(() => { - if (GestureOverlay.Instance) { - GestureOverlay.Instance.KeepPrimitiveMode = keepPrim; - } - if (Object.values(GestureUtils.Gestures).includes(tool as any)) { - if (GestureOverlay.Instance.InkShape === tool && !keepPrim) { - Doc.ActiveTool = InkTool.None; - GestureOverlay.Instance.InkShape = undefined; - } else { - Doc.ActiveTool = InkTool.Pen; - GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures; - } - } else if (tool) { - // pen or eraser - if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { - Doc.ActiveTool = InkTool.None; - } else { - Doc.ActiveTool = tool as any; - GestureOverlay.Instance.InkShape = undefined; - } - } else { - Doc.ActiveTool = InkTool.None; - } - }); -} - -ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - // prettier-ignore - const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ - ['inkMask', { - checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'), - setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask), - setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), - }], - ['fillColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"), - setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), - setMode: () => SetActiveFillColor(StrCast(value)), - }], - [ 'strokeWidth', { - checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()), - setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)), - setMode: () => SetActiveInkWidth(value.toString()), - }], - ['strokeColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()), - setInk: (doc: Doc) => (doc.color = String(value)), - setMode: () => SetActiveInkColor(StrCast(value)), - }], - ]); - - if (checkResult) { - return map.get(option)?.checkResult(); - } - map.get(option)?.setMode(); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => map.get(option)?.setInk(doc)); -}); - -/** WEB - * webSetURL - **/ -ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) { - const selected = SelectionManager.Views().lastElement(); - if (selected?.rootDoc.type === DocumentType.WEB) { - if (checkResult) { - return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); - } - selected.ComponentView?.setData?.(url); - //selected.rootDoc.data = new WebField(url); - } -}); -ScriptingGlobals.add(function webForward(checkResult?: boolean) { - const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox; - if (checkResult) { - return selected?.forward(checkResult) ? undefined : 'lightGray'; - } - selected?.forward(); -}); -ScriptingGlobals.add(function webBack(checkResult?: boolean) { - const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox; - if (checkResult) { - return selected?.back(checkResult) ? undefined : 'lightGray'; - } - selected?.back(); -}); - -/** Schema - * toggleSchemaPreview - **/ -ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - if (checkResult && selected) { - const result: boolean = NumCast(selected.schema_previewWidth) > 0; - if (result) return Colors.MEDIUM_BLUE; - else return 'transparent'; - } else if (selected) { - if (NumCast(selected.schema_previewWidth) > 0) { - selected.schema_previewWidth = 0; - } else { - selected.schema_previewWidth = 200; - } - } -}); -ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - if (checkResult && selected) { - return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (selected) { - selected._schema_singleLine = !selected._schema_singleLine; - } -}); - -/** STACK - * groupBy - */ -ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) { - SelectionManager.Docs().map(doc => (doc._fontFamily = key)); - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (checkResult) { - return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - } - if (editorView) RichTextMenu.Instance.setFontFamily(key); - else Doc.UserDoc().fontFamily = key; -}); diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx deleted file mode 100644 index 74c3c563c..000000000 --- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { Component } from 'react'; -import { BoolCast, StrCast } from '../../../../../fields/Types'; -import { IButtonProps } from '../ButtonInterface'; -import { ColorState, SketchPicker } from 'react-color'; -import { ScriptField } from '../../../../../fields/ScriptField'; -import { Doc } from '../../../../../fields/Doc'; -import { FontIconBox } from '../FontIconBox'; - -export class ColorDropdown extends Component<IButtonProps> { - render() { - const active: string = StrCast(this.props.rootDoc.dropDownOpen); - - const script: string = StrCast(this.props.rootDoc.script); - const scriptCheck: string = script + '(undefined, true)'; - const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; - - const stroke: boolean = false; - // if (script === "setStrokeColor") { - // stroke = true; - // const checkWidth = ScriptField.MakeScript("setStrokeWidth(0, true)")?.script.run().result; - // const width = 20 + (checkWidth / 100) * 70; - // const height = 20 + (checkWidth / 100) * 70; - // strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />); - // } - - const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb']; - - const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />; - const label = - !this.props.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}> - {this.props.label} - </div> - ); - - const dropdownCaret = ( - <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}> - <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" /> - </div> - ); - - const click = (value: ColorState) => { - const hex: string = value.hex; - const s = ScriptField.MakeScript(script + '("' + hex + '", false)'); - if (s) { - s.script.run().result; - } - }; - return ( - <div - className={`menuButton ${this.props.type} ${active}`} - style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }} - onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)} - onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} /> - <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} /> - {label} - {/* {dropdownCaret} */} - {this.props.rootDoc.dropDownOpen ? ( - <div> - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> - {colorBox(click)} - </div> - <div - className="dropbox-background" - onClick={e => { - e.stopPropagation(); - this.props.rootDoc.dropDownOpen = false; - }} - /> - </div> - ) : null} - </div> - ); - } -} diff --git a/src/client/views/nodes/button/colorDropdown/index.ts b/src/client/views/nodes/button/colorDropdown/index.ts deleted file mode 100644 index 1147d6457..000000000 --- a/src/client/views/nodes/button/colorDropdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ColorDropdown';
\ No newline at end of file diff --git a/src/client/views/nodes/button/textButton/TextButton.tsx b/src/client/views/nodes/button/textButton/TextButton.tsx deleted file mode 100644 index 5d7d55863..000000000 --- a/src/client/views/nodes/button/textButton/TextButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { Component } from 'react'; -import { BoolCast } from '../../../../../fields/Types'; -import { IButtonProps } from '../ButtonInterface'; - -export class TextButton extends Component<IButtonProps> { - render() { - const type = this.props.type; - // Determine the type of toggle button - const buttonText: boolean = BoolCast(this.props.rootDoc.switchToggle); - - return ( - <div - className={`menuButton ${this.props.type}`} - style={{ - opacity: 1, - backgroundColor: this.props.backgroundColor, - color: this.props.color, - }} - > - <FontAwesomeIcon - className={`fontIconBox-icon-${this.props.type}`} - icon={this.props.icon} - color={this.props.color} - /> - {this.props.label} - </div> - ); - } -} diff --git a/src/client/views/nodes/button/textButton/index.ts b/src/client/views/nodes/button/textButton/index.ts deleted file mode 100644 index 01d62eb7e..000000000 --- a/src/client/views/nodes/button/textButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './TextButton';
\ No newline at end of file diff --git a/src/client/views/nodes/button/toggleButton/ToggleButton.tsx b/src/client/views/nodes/button/toggleButton/ToggleButton.tsx deleted file mode 100644 index dca6487d8..000000000 --- a/src/client/views/nodes/button/toggleButton/ToggleButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { Component } from 'react'; -import { BoolCast } from '../../../../../fields/Types'; -import { Colors } from '../../../global/globalEnums'; -import { IButtonProps } from '../ButtonInterface'; - -export class ToggleButton extends Component<IButtonProps> { - render() { - const type = this.props.type; - // Determine the type of toggle button - const switchToggle: boolean = BoolCast(this.props.rootDoc.switchToggle); - - if (switchToggle) { - return ( - <div className={`menuButton ${type} ${'switch'}`}> - <label className="switch"> - <input type="checkbox" - checked={this.props.backgroundColor === Colors.MEDIUM_BLUE} - /> - <span className="slider round"></span> - </label> - </div> - ); - } else { - return ( - <div className={`menuButton ${type}`} - style={{ opacity: 1, backgroundColor: this.props.backgroundColor, color: this.props.color }}> - <FontAwesomeIcon className={`fontIconBox-icon-${type}`} icon={this.props.icon} color={this.props.color} /> - {this.props.label} - </div> - ); - } - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/button/toggleButton/index.ts b/src/client/views/nodes/button/toggleButton/index.ts deleted file mode 100644 index cdb9c527c..000000000 --- a/src/client/views/nodes/button/toggleButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ToggleButton';
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index c929b7ff3..48f4c2afd 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -2,8 +2,9 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; +import { NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -210,14 +211,14 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} renderDepth={this._textBox.props.renderDepth + 1} - PanelWidth={this._finalLayout[WidthSym]} - PanelHeight={this._finalLayout[HeightSym]} + PanelWidth={this._finalLayout[Width]} + PanelHeight={this._finalLayout[Height]} focus={this.outerFocus} whenChildContentsActiveChanged={returnFalse} bringToFront={emptyFunction} dontRegisterView={false} - docFilters={this.props.tbox?.props.docFilters} - docRangeFilters={this.props.tbox?.props.docRangeFilters} + childFilters={this.props.tbox?.props.childFilters} + childFiltersByRanges={this.props.tbox?.props.childFiltersByRanges} searchFilterDocs={this.props.tbox?.props.searchFilterDocs} /> </div> diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 6c61f6709..d5ad128fe 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, Field } from '../../../../fields/Doc'; +import { Doc } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; @@ -17,6 +17,7 @@ import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; import React = require('react'); +import { Transform } from '../../../util/Transform'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -113,6 +114,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna componentWillUnmount() { this._reactionDisposer?.(); } + return100 = () => 100; // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { @@ -123,7 +125,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this.props.hideKey ? undefined : () => 100} + maxWidth={this.props.hideKey ? undefined : this.return100} columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} selectedCell={() => [this._dashDoc!, 0]} fieldKey={this._fieldKey} @@ -135,6 +137,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna allowCRs={true} oneLine={!this._expanded} finishEdit={action(() => (this._expanded = false))} + transform={Transform.Identity} + menuTarget={null} /> </div> ); @@ -144,7 +148,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.rootDoc); - embedding._viewType = CollectionViewType.Time; + embedding._type_collection = CollectionViewType.Time; const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders'; let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField)); if (!list) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 109b62e6f..348bdd79e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -84,7 +84,6 @@ audiotag:hover { height: 11; } -.formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; @@ -92,9 +91,6 @@ audiotag:hover { width: 100%; height: unset; } -.formattedTextBox-outer-selected { - cursor: text; -} .formattedTextBox-sidebar-handle { position: absolute; @@ -148,10 +144,8 @@ audiotag:hover { } .formattedTextBox-inner-rounded, -.formattedTextBox-inner-rounded-selected, .formattedTextBox-inner, -.formattedTextBox-inner-minimal, -.formattedTextBox-inner-selected { +.formattedTextBox-inner-minimal { height: 100%; white-space: pre-wrap; .ProseMirror:hover { @@ -169,17 +163,6 @@ audiotag:hover { border-width: 1px; } } -.formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner-selected { - > .ProseMirror { - padding: 10px; - } -} -.formattedTextBox-outer-selected { - > .ProseMirror:hover { - background: unset; - } -} .gpt-typing-wrapper { padding: 10px; @@ -640,7 +623,6 @@ footnote::before { } } - .formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 023814a9d..1dcc445e8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -12,9 +12,11 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; +import { List } from '../../../../fields/List'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { RichTextUtils } from '../../../../fields/RichTextUtils'; @@ -32,8 +34,8 @@ import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; -import { IsFollowLinkScript } from '../../../util/LinkFollower'; 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'; @@ -68,8 +70,6 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { RTFMarkup } from '../../../util/RTFMarkup'; -import { List } from '../../../../fields/List'; const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @@ -96,6 +96,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; public _applyingChange: string = ''; + private _finishingLink = false; private _searchIndex = 0; private _lastTimedMark: Mark | undefined = undefined; private _cachedLinks: Doc[] = []; @@ -131,7 +132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get layout_autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight; @@ -243,9 +244,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc; - const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true }); + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc }); this.addDocument(anchor); + this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); + this._finishingLink = false; PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc); return anchor; }; @@ -296,21 +299,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; dispatchTransaction = (tx: Transaction) => { - if (this._editorView) { + if (this._editorView && (this._editorView as any).docView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; - const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box - const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype - const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template - const json = JSON.stringify(state.toJSON()); + const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); + const newJson = JSON.stringify(state.toJSON()); + const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box + const templateData = this.rootDoc !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template + const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); - const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); + const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, ''); - if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) { + if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { @@ -320,29 +323,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined; let unchanged = true; - if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { + if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) { this._applyingChange = this.fieldKey; - const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text; + const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); - if ((!curTemp && !curProto) || curText || json.includes('dash')) { + if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (removeSelection(json) !== removeSelection(curLayout?.Data)) { + if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); - if (numstring !== undefined) { - dataDoc[this.fieldKey] = Number(curText); - } else { - dataDoc[this.fieldKey] = new RichTextField(json, curText); - } - dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited - textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); + dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited + textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } } else { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have - ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } this._applyingChange = ''; @@ -418,6 +417,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); + this.prepareForTyping(); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); }; @@ -460,8 +460,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? - (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) || - DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!); + (LinkManager.Links(this.rootDoc).find( + link => + Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && // + Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) + ) || + DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); @@ -534,36 +538,50 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); + if (de.complete.annoDragData) { + de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); + e.stopPropagation(); + return true; + } const dragData = de.complete.docDragData; if (dragData) { - const draggedDoc = dragData.draggedDocuments.length && dragData.draggedDocuments[0]; - // replace text contents whend dragging with Alt - if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { - if (draggedDoc.data instanceof RichTextField) { - Doc.GetProto(this.dataDoc)[this.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); - e.stopPropagation(); - } - // embed document when dragg marked as embed - } else if (de.embedKey) { - const target = dragData.droppedDocuments[0]; - const node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), - height: target[HeightSym](), - title: 'dashDoc', - docId: target[Id], - float: 'unset', - }); - if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { - dragData.removeDocument?.(dragData.draggedDocuments[0]); + const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const effectiveAcl = GetEffectiveAcl(dataDoc); + let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl); + const draggedDoc = dragData.draggedDocuments.lastElement(); + if (added) { + // replace text contents when dragging with Alt + if (de.altKey) { + const fieldKey = Doc.LayoutFieldKey(draggedDoc); + if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); + } + + // embed document when drag marked as embed + } else if (de.embedKey) { + const node = schema.nodes.dashDoc.create({ + width: draggedDoc[Width](), + height: draggedDoc[Height](), + title: 'dashDoc', + docId: draggedDoc[Id], + float: 'unset', + }); + if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { + added = dragData.removeDocument?.(draggedDoc) ? true : false; + } + if (added) { + 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)); + } } - target._freeform_fitContentsToBox = true; - target.embedContainer = this.rootDoc; - const view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); - e.stopPropagation(); } // otherwise, fall through to outer collection to handle drop + !added && e.preventDefault(); + e.stopPropagation(); + return added; } + return false; }; getNodeEndpoints(context: Node, node: Node): { from: number; to: number } | null { @@ -650,7 +668,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } - this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) + this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @@ -667,7 +685,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { - const batch = UndoManager.StartBatch('sidebar'); + const batch = UndoManager.StartBatch('toggle sidebar'); setupMoveUpEvents( this, e, @@ -686,7 +704,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; - const width = this.layoutDoc[WidthSym]() + localDelta[0]; + const width = this.layoutDoc[Width]() + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -694,13 +712,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return false; }; - @undoBatch deleteAnnotation = (anchor: Doc) => { + const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); this.props.select(false); + setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch @@ -733,17 +752,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .split(' ') .filter(h => h); const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0]; + const deleteMarkups = undoBatch(() => { + const sel = editor.state.selection; + editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + }); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( action(anchor => { + anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc); AnchorMenu.Instance.Status = 'annotation'; - AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc); + AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc); AnchorMenu.Instance.Pinned = false; - AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc); - AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc); - AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc); - AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc); + AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc); + AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc); + AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc); + AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); }) ); @@ -808,9 +832,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); uicontrols.push({ - description: (this.Document._layout_altContentUI ? 'Hide' : 'Show') + ' Alt Content UI', - event: () => (this.layoutDoc._layout_altContentUI = !this.layoutDoc._layout_altContentUI), - icon: !this.Document._layout_altContentUI ? 'eye-slash' : 'eye', + description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', + 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 && @@ -823,7 +847,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const appearance = cm.findByDescription('Appearance...'); const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; - appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); + appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); !Doc.noviceMode && @@ -862,11 +886,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); - optionItems.push({ - description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), - icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', - }); + this.props.renderDepth && + optionItems.push({ + description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', + event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', + }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, @@ -906,7 +931,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text); if (image_url) { const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] }); - const source = Utils.prepend(result.accessPaths.agnostic.client); + const source = result.accessPaths.agnostic.client; const newDoc = Docs.Create.ImageDocument(source, { x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10, y: NumCast(this.rootDoc.y), @@ -915,10 +940,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps data_nativeWidth: result.nativeWidth, data_nativeHeight: result.nativeHeight, }); - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + if (Doc.IsInMyOverlay(this.rootDoc)) { newDoc.overlayX = this.rootDoc.x; newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + Doc.AddToMyOverlay(newDoc); } else { this.props.addDocument?.(newDoc); } @@ -960,7 +985,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._editorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { - const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor', unrendered: true }); + const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; }; const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); @@ -998,7 +1023,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); if (sel.from !== sel.to) { - const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true }); + const anchor = + anchorDoc ?? + Docs.Create.ConfigDocument({ + // + title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')', + annotationOn: this.dataDoc, + }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { @@ -1014,6 +1045,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; + anchor.title = selectedText.substring(0, 30); return anchor; } return anchorDoc ?? this.rootDoc; @@ -1022,7 +1054,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } getView = async (doc: Doc) => { - if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { !this.SidebarShown && this.toggleSidebar(false); setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } @@ -1129,11 +1161,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); - if (layout_autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) { + if ( + (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this.props.isSelected()) && // + layout_autoHeight && + newHeight && + newHeight !== this.rootDoc.height && + !this.props.dontRegisterView + ) { this.props.setHeight?.(newHeight); } }, - { fireImmediately: true } + { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } ); this._disposers.links = reaction( () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks @@ -1155,7 +1193,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps () => { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(whichDoc[this.fieldKey]) }; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey])) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1199,15 +1237,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.selected = reaction( () => this.props.isSelected(), action(selected => { + selected && this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { - this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed + this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } if (this._editorView && selected) { RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); - this.autoLink(); + setTimeout(this.autoLink, 20); } // Accessing editor and text doc for gpt assisted text edits if (this._editorView && selected) { @@ -1474,11 +1513,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); - if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = ''; this.props.select(false); - if (selLoadChar && this._editorView) { + if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; @@ -1488,30 +1527,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size) .setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) { + } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); - this.startUndoTypingBatch(); - } else if (this._editorView) { - this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } } selectOnLoad && this._editorView!.focus(); - // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + if (this.props.isContentActive()) this.prepareForTyping(); if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; - // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated. + // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated. if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); - this._editorView.state.storedMarks = [ - ...(this._editorView.state.storedMarks ?? []), - ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), - ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), - ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), - ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), - ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ]; + if (FormattedTextBox.PasteOnLoad) { const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); FormattedTextBox.PasteOnLoad = undefined; @@ -1521,9 +1548,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.DontSelectInitialText = false; } + // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + prepareForTyping = () => { + this._editorView?.dispatch( + this._editorView?.state.tr.setStoredMarks([ + ...(this._editorView.state.storedMarks?.filter(mark => ![schema.marks.em, schema.marks.underline, schema.marks.pFontFamily, schema.marks.pFontSize, schema.marks.strong, schema.marks.pFontColor].includes(mark.type)) ?? []), + ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], + ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), + ]) + ); + }; + + @action componentWillUnmount() { + if (this._recording) { + this._recording = !this._recording; + } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; this.unhighlightSearchTerms(); this._editorView?.destroy(); RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); @@ -1558,8 +1607,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } if (this._recording && !e.ctrlKey && e.button === 0) { this.breakupDictation(); - e.preventDefault(); - e.stopPropagation(); } this._downX = e.clientX; this._downY = e.clientY; @@ -1624,7 +1671,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); - this.startUndoTypingBatch(); + e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { @@ -1699,13 +1746,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } startUndoTypingBatch() { - !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping')); + !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title)); } public endUndoTypingBatch() { - const wasUndoing = this._undoTyping; this._undoTyping?.end(); this._undoTyping = undefined; - return wasUndoing; } @action @@ -1733,7 +1778,7 @@ 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.sidebarViewType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { + if (this.layoutDoc.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { try { translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => { setTimeout( @@ -1799,8 +1844,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': - [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) && - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); + if (e.code !== 'Space') { + [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.rootDoc)) && + this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); + } + break; } this.startUndoTypingBatch(); }; @@ -1822,17 +1870,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tryUpdateScrollHeight = () => { const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children) { + if (children && !SnappingManager.GetIsDragging()) { const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); const toHgt = (node: Element) => { const { height, marginTop, marginBottom } = getComputedStyle(node); return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight); if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { @@ -1941,25 +1990,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps fitContentsToBox={this.fitContentsToBox} noSidebar={true} treeViewHideTitle={true} - fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} + fieldKey={this.layoutDoc.sidebar_collectionType === '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.sidebarViewType))} + {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} </div> ); } cycleAlternateText = () => { - if (this.layoutDoc._layout_altContentUI) { - const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`]; + if (this.layoutDoc._layout_enableAltContentUI) { + const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; } }; @computed get overlayAlternateIcon() { - const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`]; + const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -1995,62 +2044,69 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; + onPassiveWheel = (e: WheelEvent) => { + // 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(); + e.stopPropagation(); + } + }; + _oldWheel: any; + @computed get fontColor() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + } + @computed get fontSize() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize); + } + @computed get fontFamily() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily); + } + @computed get fontWeight() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight); + } render() { TraceMobx(); - const active = this.props.isContentActive() || this.props.isSelected(); - const selected = active; const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); - const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : ''; - const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); - if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); - const minimal = this.props.ignoreAutoHeight; + const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; + setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const selPad = (selected && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; - const selPaddingClass = selected && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : ''; const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} - 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 (this.props.isContentActive() && !this.props.allowScroll) { - if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - } - }, - { passive: false } - ) - } + ref={r => { + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }} style={{ ...(this.props.dontScale ? {} : { transform: `scale(${scale})`, - transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%`, }), - display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, + // display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, transition: 'inherit', // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, - color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), - fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), - fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight), + color: this.fontColor, + fontSize: this.fontSize, + fontFamily: this.fontFamily, + fontWeight: this.fontWeight, ...styleFromLayoutString, }}> <div className="formattedTextBox-cont" ref={this._ref} style={{ + cursor: this.props.isContentActive() ? 'text' : undefined, overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - pointerEvents: interactive ? undefined : 'none', + pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2062,32 +2118,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer${selected ? '-selected' : ''}`} + className={`formattedTextBox-outer`} ref={this._scrollRef} style={{ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, - pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`} + className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), - paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined, + paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`), + paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`), + paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), + paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), }} /> </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.audioHandle} - {this.layoutDoc._layout_altContentUI ? this.overlayAlternateIcon : null} + {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 4dfe07b24..112a0d87e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -4,7 +4,7 @@ import { Schema } from 'prosemirror-model'; import { splitListItem, wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; -import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; import { GetEffectiveAcl } from '../../../../fields/util'; import { Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; @@ -12,6 +12,7 @@ import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { OpenWhere } from '../DocumentView'; import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; +import { Doc } from '../../../../fields/Doc'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -48,15 +49,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const canEdit = (state: any) => { switch (GetEffectiveAcl(props.DataDoc)) { case AclAugment: - return false; - case AclSelfEdit: - for (var i = state.selection.from; i < state.selection.to; i++) { - const marks = state.doc.resolve(i)?.marks?.(); - if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) { - return false; - } + const prevNode = state.selection.$cursor.nodeBefore; + const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; + if (prevUser != Doc.CurrentUserEmail) { + return false; } - break; } return true; }; @@ -337,7 +334,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a blank space bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (!canEdit(state)) return true; + if (GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 5e0041b84..7c3e4baad 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -138,7 +138,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; @@ -221,7 +221,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._fontSize) && activeSizes.add(StrCast(dv.rootDoc._fontSize))); + SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._text_fontSize) && activeSizes.add(StrCast(dv.rootDoc._text_fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } @@ -345,8 +345,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); } } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => (dv.rootDoc._fontSize = fontSize)); - } else Doc.UserDoc()._fontSize = fontSize; + SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize)); + } else Doc.UserDoc().fontSize = fontSize; this.updateMenu(this.view, undefined, this.props); }; @@ -355,7 +355,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); - } else Doc.UserDoc()._fontFamily = family; + } else Doc.UserDoc().fontFamily = family; this.updateMenu(this.view, undefined, this.props); }; @@ -758,11 +758,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // <div className="collectionMenu-divider" key="divider 3" /> // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { // this.activeFontSize = val; - // SelectionManager.Views().map(dv => dv.props.Document._fontSize = val); + // SelectionManager.Views().map(dv => dv.props.Document._text_fontSize = val); // })), // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { // this.activeFontFamily = val; - // SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); + // SelectionManager.Views().map(dv => dv.props.Document._text_fontFamily = val); // })), // <div className="collectionMenu-divider" key="divider 4" />, // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 104aed058..8bafc2cef 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,11 +1,11 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { DataSym, Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, StrListCast } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { ComputedField } from '../../../../fields/ScriptField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { normalizeEmail } from '../../../../fields/util'; +import { NumCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -76,12 +76,21 @@ export class RichTextRules { //Create annotation to a field on the text document new InputRule(new RegExp(/>>$/), (state, match, start, end) => { - const textDoc = this.Document[DataSym]; + const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument('', { _layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _layout_fitWidth: true, _layout_autoHeight: true, _fontSize: '9px', title: 'inline comment' }); + const textDocInline = Docs.Create.TextDocument('', { + _layout_fieldKey: inlineLayoutKey, + _width: 75, + _height: 35, + annotationOn: textDoc, + _layout_fitWidth: true, + _layout_autoHeight: true, + _text_fontSize: '9px', + title: 'inline comment', + }); textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point @@ -234,13 +243,13 @@ export class RichTextRules { // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // [[<fieldKey> : <Doc>]] - // [[:Doc]] => hyperlink + // [[:docTitle]] => hyperlink // [[fieldKey]] => show field // [[fieldKey=value]] => show field and also set its value - // [[fieldKey:Doc]] => show field of doc + // [[fieldKey:docTitle]] => show field of doc new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const docId = match[3]?.replace(':', ''); + const docTitle = match[3]?.replace(':', ''); const value = match[2]?.substring(1); const linkToDoc = (target: Doc) => { const rstate = this.TextBox.EditorView?.state; @@ -257,21 +266,25 @@ export class RichTextRules { } }; if (!fieldKey) { - if (docId) { - const target = DocServer.QUERY_SERVER_CACHE(docId); - if (target) setTimeout(() => linkToDoc(target)); - else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId))); - - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + if (docTitle) { + const target = DocServer.FindDocByTitle(docTitle); + if (target) { + setTimeout(() => linkToDoc(target)); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + } } return state.tr; } if (value !== '' && value !== undefined) { const num = value.match(/^[0-9.]$/); - this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; + 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); } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false }); - return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); + return state.tr; }), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document @@ -303,11 +316,11 @@ export class RichTextRules { new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - //this.Document[DataSym]['#' + tag] = '#' + tag; - const tags = StrListCast(this.Document[DataSym].tags); + //this.Document[DocData]['#' + tag] = '#' + tag; + const tags = StrListCast(this.Document[DocData].tags); if (!tags.includes(tag)) { tags.push(tag); - this.Document[DataSym].tags = new List<string>(tags); + this.Document[DocData].tags = new List<string>(tags); } const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag }); return state.tr diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx new file mode 100644 index 000000000..58f0b29e4 --- /dev/null +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -0,0 +1,38 @@ +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../../Utils'; +import { Transform } from '../../../util/Transform'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { DefaultStyleProvider } from '../../StyleProvider'; +import { DocumentView, DocumentViewInternal } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import React = require('react'); + +@observer +export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ImportElementBox, fieldKey); + } + + screenToLocalXf = () => this.props.ScreenToLocalTransform().scale(1 * (this.props.NativeDimScaling?.() || 1)); + @computed get mainItem() { + return ( + <div style={{ backgroundColor: 'pink' }}> + <DocumentView + {...this.props} // + LayoutTemplateString={undefined} + Document={this.rootDoc} + isContentActive={returnFalse} + DataDoc={undefined} + addDocument={returnFalse} + ScreenToLocalTransform={this.screenToLocalXf} + hideResizeHandles={true} + /> + </div> + ); + } + render() { + return !(this.rootDoc instanceof Doc) ? null : this.mainItem; + } +} diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index eb91c82f3..bf56b4d9e 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -951,6 +951,7 @@ margin-right: unset; height: 100%; position: relative; + user-select: none; } select { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 0a907c958..c0cd3ab70 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,8 @@ 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 { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -41,8 +42,7 @@ export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; pannable?: boolean; - map?:boolean; - viewType?: boolean; + type_collection?: boolean; inkable?: boolean; filters?: boolean; pivot?: boolean; @@ -104,10 +104,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; @computed get isTreeOrStack() { - return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any); + return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any); } @computed get isTree() { - return this.layoutDoc._viewType === CollectionViewType.Tree; + return this.layoutDoc._type_collection === CollectionViewType.Tree; } @computed get presFieldKey() { return StrCast(this.layoutDoc.presFieldKey, 'data'); @@ -129,14 +129,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static targetRenderedDoc = (doc: Doc) => { const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null); - return targetDoc?.unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; + return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { - if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true; + if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true; return false; } @computed get panable() { - if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; + if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; return false; } @computed get selectedDocumentView() { @@ -197,7 +197,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.turnOffEdit(true); this._disposers.selection = reaction( () => SelectionManager.Views(), - views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation() + views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(), + { fireImmediately: true } ); this._disposers.editing = reaction( () => this.layoutDoc.presStatus === PresStatus.Edit, @@ -275,7 +276,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presIndexed !== undefined && targetList) { const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); - return listItems.filter(doc => !doc.unrendered); + return listItems.filter(doc => !doc.layout_unrendered); } }; // Called when the user activates 'next' - to move to the next part of the pres. trail @@ -299,8 +300,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { listItemDoc.presTransition = 500; targetView?.setAnimEffect(listItemDoc, 500); if (targetView?.docView && this.activeItem.presBulletExpand) { - targetView.docView._animateScalingTo = 1.1; - Doc.AddUnHighlightWatcher(() => (targetView!.docView!._animateScalingTo = 0)); + targetView.docView._animateScalingTo = 1.2; + targetView.docView._animateScaleTime = 400; + Doc.AddUnHighlightWatcher(() => { + targetView.docView!._animateScaleTime = undefined; + targetView!.docView!._animateScalingTo = 0; + }); } listItemDoc.opacity = undefined; this.activeItem.presIndexed = presIndexed + 1; @@ -331,7 +336,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action back = () => { const activeItem: Doc = this.activeItem; - const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem.presMovement; @@ -377,26 +381,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { static pinDataTypes(target?: Doc): pinDataTypes { const targetType = target?.type as any; const inkable = [DocumentType.INK].includes(targetType); - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking; - const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform); + 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); const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; - const viewType = targetType === DocumentType.COL; + const type_collection = targetType === DocumentType.COL; const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, viewType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action playAnnotation = (anno: AudioField) => {}; @action static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { - const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); + const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); if (!bestTarget || activeItem === bestTarget) return; let changed = false; if (pinDocLayout) { @@ -499,16 +503,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } - if ((pinDataTypes?.viewType && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) { - if (bestTarget._viewType !== activeItem.presViewType) { - bestTarget._viewType = activeItem.presViewType; + if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) { + if (bestTarget._type_collection !== activeItem.presViewType) { + bestTarget._type_collection = activeItem.presViewType; changed = true; } } if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) { - if (bestTarget.docFilters !== activeItem.presDocFilters) { - bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]); + if (bestTarget.childFilters !== activeItem.presDocFilters) { + bestTarget.childFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]); changed = true; } } @@ -537,13 +541,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) { const fkey = Doc.LayoutFieldKey(bestTarget); - const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.unrendered); + const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); const newItems = DocListCast(activeItem.presAnnotations).map(doc => { doc.hidden = false; return doc; }); const hiddenItems = DocListCast(bestTarget[fkey + '_annotations']) - .filter(doc => !doc.unrendered && !newItems.includes(doc)) + .filter(doc => !doc.layout_unrendered && !newItems.includes(doc)) .map(doc => { doc.hidden = true; return doc; @@ -623,7 +627,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinProps.pinData.scrollable || pinProps.pinData.temporal || pinProps.pinData.pannable || - pinProps.pinData.viewType || + pinProps.pinData.type_collection || pinProps.pinData.clippable || pinProps.pinData.datarange || pinProps.pinData.dataview || @@ -636,7 +640,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinProps.pinData.dataannos) { const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.unrendered)); + pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); } if (pinProps.pinData.inkable) { pinDoc.presFillColor = targetDoc.fillColor; @@ -673,8 +677,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }) ) ); - if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType; - if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField); + if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection; + if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField); if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField; if (pinProps.pinData.pannable) { pinDoc.presPanX = NumCast(targetDoc._freeform_panX); @@ -711,7 +715,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const finished = () => { afterNav?.(); console.log('Finish Slide Nav: ' + targetDoc.title); - targetDoc[AnimationSym] = undefined; + targetDoc[Animation] = undefined; }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); @@ -757,7 +761,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (targetDoc) { - if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined; + if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[Animation] = undefined; DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { // if target or the doc it annotates is not in the lightbox, then close the lightbox @@ -821,7 +825,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const savedStates = docs.map(doc => { switch (doc.type) { case DocumentType.COL: - if (doc._viewType === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; + if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; break; case DocumentType.INK: if (doc.data instanceof InkField) { @@ -839,8 +843,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case CollectionViewType.Freeform: { const { x, y, s, doc } = savedState!; - doc._panX = x; - doc._panY = y; + doc._freeform_panX = x; + doc._freeform_panY = y; doc._freeform_scale = s; } break; @@ -857,7 +861,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }); LightboxView.SetLightboxDoc(undefined); - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); + Doc.RemFromMyOverlay(this.rootDoc); return PresStatus.Edit; }; }; @@ -953,8 +957,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]); }; exitMinimize = () => { - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); + if (Doc.IsInMyOverlay(this.layoutDoc)) { + Doc.RemFromMyOverlay(this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right); } return PresStatus.Edit; @@ -966,7 +970,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc.overlayY = pt[1]; doc._height = 30; doc._width = PresBox.minimizedWidth; - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); return (doc.presStatus = PresStatus.Manual); } @@ -978,11 +982,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch viewChanged = action((e: React.ChangeEvent) => { //@ts-ignore - const viewType = e.target.selectedOptions[0].value as CollectionViewType; - this.layoutDoc.presFieldKey = this.fieldKey + (viewType === CollectionViewType.Tree ? '-linearized' : ''); + const type_collection = e.target.selectedOptions[0].value as CollectionViewType; + this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : ''); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here - [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined); - this.rootDoc._viewType = viewType; + [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.rootDoc._pivotField = undefined); + this.rootDoc._type_collection = type_collection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } @@ -1044,13 +1048,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); return true; }; - childLayoutTemplate = () => (!this.isTreeOrStack ? undefined : DocCast(Doc.UserDoc().presElement)); + childLayoutTemplate = () => (!this.isTreeOrStack ? DocCast(Doc.UserDoc().presElement) : DocCast(Doc.UserDoc().presElement)); removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; - isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction); - //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false; - /** * For sorting the array so that the order is maintained when it is dropped. */ @@ -1183,7 +1184,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } break; case 'Escape': - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { + if (Doc.IsInMyOverlay(this.layoutDoc)) { this.exitClicked(); } else if (this.layoutDoc.presStatus === PresStatus.Edit) { this.clearSelectedArray(); @@ -1452,6 +1453,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { min={min} max={max} value={value} + readOnly={true} style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { @@ -1525,7 +1527,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton"> <div className="presBox-subheading">Slide Duration</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + <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-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> @@ -1678,7 +1680,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />% + <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-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> @@ -1693,7 +1695,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + <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-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> @@ -1780,6 +1782,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presStartTime).toFixed(2)} onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { @@ -1806,6 +1809,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presEndTime).toFixed(2)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presEndTime = Number(e.target.value); @@ -2088,20 +2092,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { createTemplate = (layout: string, input?: string) => { const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0; const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0; - const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); - const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); - const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); - const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); - const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); - const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); - const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' }); + const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' }); + const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' }); + const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' }); + const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' }); + const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' }); + const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' }); // prettier-ignore switch (layout) { case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); - case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y }) + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }) } }; @@ -2150,12 +2154,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get toolbar() { const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); const activeColor = Colors.LIGHT_BLUE; const inactiveColor = Colors.WHITE; - return mode === CollectionViewType.Carousel3D || inOverlay ? null : ( + 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)}> <FontAwesomeIcon icon={"plus"} /> @@ -2194,11 +2197,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * presentPanel: The button to start the presentation / open minimized view of the presentation */ @computed get topPanel() { - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return ( - <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> + <div + className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} + style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> {isMini ? null : ( <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}> @@ -2248,7 +2252,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get playButtons() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); + const inOverlay = Doc.IsInMyOverlay(this.rootDoc); // Case 1: There are still other frames and should go through all frames before going to next slide return ( <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}> @@ -2419,10 +2423,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> <div @@ -2465,7 +2468,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> ) : ( - <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}> + <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}> {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} @@ -2479,9 +2482,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} + childDragAction="move" //childLayoutFitWidth={returnTrue} childOpacity={returnOne} - //childLayoutString={PresElementBox.LayoutString('data')} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index e48d2d674..f31cf6147 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,19 +1,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +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 { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; @@ -106,8 +107,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalTransform={Transform.Identity} renderDepth={this.props.renderDepth + 1} docViewPath={returnEmptyDoclist} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} rootSelected={returnTrue} addDocument={returnFalse} @@ -191,13 +192,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragArray = this.presBoxView?._dragArray ?? []; const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); - dragData.dropAction = 'move'; - dragData.treeViewDoc = this.presBox?._viewType === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; + dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; dragData.moveDocument = this.props.moveDocument; const dragItem: HTMLElement[] = []; + const classesToRestore = new Map<HTMLElement, string>(); if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; if (doc) { + classesToRestore.set(doc, doc.className); doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; dragItem.push(doc); } @@ -211,16 +213,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { dragItem.push(doc); } - // const dropEvent = () => runInAction(() => this._dragging = false); if (activeItem) { + runInAction(() => (this._dragging = true)); DragManager.StartDocumentDrag( dragItem.map(ele => ele), dragData, e.clientX, e.clientY, - undefined + undefined, + action(() => { + Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); + this._dragging = false; + }) ); - // runInAction(() => this._dragging = true); return true; } return false; @@ -263,16 +268,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - @undoBatch - removeItem = action((e: React.MouseEvent) => { + removePresentationItem = undoable((e: React.MouseEvent) => { e.stopPropagation(); if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { - this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; + runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1)); } this.props.removeDocument?.(this.rootDoc); this.presBoxView?.removeFromSelectedArray(this.rootDoc); this.removeAllRecordingInOverlay(); - }); + }, 'Remove doc from pres trail'); // set the value/title of the individual pres element @undoBatch @@ -323,11 +327,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; removeAllRecordingInOverlay = () => { - DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { - if (doc.slides === this.rootDoc) { - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); - } - }); + DocListCast(Doc.MyOverlayDocs.data) + .filter(doc => doc.slides === this.rootDoc) + .forEach(Doc.RemFromMyOverlay); }; static removeEveryExistingRecordingInOverlay = () => { @@ -335,9 +337,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { if (doc.slides !== null) { // if it's a recording video, don't remove from overlay (user can lose data) - if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return; - - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); + if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) { + Doc.RemFromMyOverlay(doc); + } } }); }; @@ -357,7 +359,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); if (activeItem.recording) { - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, DocCast(activeItem.recording)); + Doc.AddToMyOverlay(DocCast(activeItem.recording)); } }; @@ -369,7 +371,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording - // Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); + // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null)); } else { // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode @@ -379,9 +381,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const recording = Docs.Create.WebCamDocument('', { _width: 384, _height: 216, - hideDocumentButtonBar: true, + layout_hideDocumentButtonBar: true, layout_hideDecorationTitle: true, - hideOpenButton: true, + layout_hideOpenButton: true, // hideDeleteButton: true, cloneFieldFilter: new List<string>(['isSystem']), }); @@ -391,9 +393,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.recording = recording; // make recording box appear in the bottom right corner of the screen - recording.overlayX = window.innerWidth - recording[WidthSym]() - 20; - recording.overlayY = window.innerHeight - recording[HeightSym]() - 20; - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording); + recording.overlayX = window.innerWidth - recording[Width]() - 20; + recording.overlayY = window.innerHeight - recording[Height]() - 20; + Doc.AddToMyOverlay(recording); } }; @@ -476,7 +478,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); items.push( <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> - <div className={'slideButton'} onClick={this.removeItem}> + <div className={'slideButton'} onClick={this.removePresentationItem}> <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> @@ -524,7 +526,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, + //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, }}> <div @@ -538,7 +540,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-number" title="select without navigation" - style={{ pointerEvents: this.presBoxView?.isContentActive() ? 'all' : undefined }} onPointerDown={e => { e.stopPropagation(); if (this._itemRef.current && this._dragRef.current) { @@ -546,7 +547,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }} onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div> - <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> + <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index d6dddf71a..e35e011e2 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -15,6 +15,9 @@ 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'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -42,7 +45,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { ]; @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)'; - @observable private _showLinkPopup: boolean = false; @observable public Status: 'marquee' | 'annotation' | '' = ''; @@ -131,7 +133,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { () => this._opacity, opacity => { if (!opacity) { - this._showLinkPopup = false; this.setGPTPopupVis(false); this.setGPTPopupText(''); } @@ -141,7 +142,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this._disposer = reaction( () => SelectionManager.Views().slice(), selected => { - this._showLinkPopup = false; this.setGPTPopupVis(false); this.setGPTPopupText(''); AnchorMenu.Instance.fadeOut(true); @@ -253,49 +253,22 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { AnchorMenu.Instance.fadeOut(true); }; - @action - toggleLinkPopup = (e: React.MouseEvent) => { - //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button - //change popup visibility field to visible - this._showLinkPopup = !this._showLinkPopup; - }; - @computed get highlighter() { - const button = ( - <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> - <div className="anchor-color-preview"> - <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} /> - <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> - </div> - </button> - ); - - const dropdownContent = ( - <div className="dropdown"> - <p>Change highlighter color:</p> - <div className="color-wrapper"> - {this._palette.map(color => { - if (color) { - return this.highlightColor === color ? ( - <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> - ) : ( - <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> - ); - } - })} - </div> - </div> - ); return ( - <Tooltip key="highlighter" title={<div className="dash-tooltip">{'Click to Highlight'}</div>}> - <div className="anchorMenu-highlighter"> - <ButtonDropdown key={'highlighter'} button={button} dropdownContent={dropdownContent} pdf={true} /> - </div> - </Tooltip> + <Group> + <IconButton + icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />} + tooltip={'Click to Highlight'} + onClick={this.highlightClicked} + colorPicker={this.highlightColor} + color={StrCast(Doc.UserDoc().userColor)} + /> + <ColorPicker selectedColor={this.highlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} /> + </Group> ); } - @action changeHighlightColor = (color: string, e: React.PointerEvent) => { + @action changeHighlightColor = (color: string) => { const col: ColorState = { hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: '' }, @@ -304,8 +277,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { oldHue: 0, source: '', }; - e.preventDefault(); - e.stopPropagation(); this.highlightColor = Utils.colorString(col); }; @@ -317,7 +288,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { canSummarize = (): boolean => { const docs = SelectionManager.Docs(); if (docs.length > 0) { - return docs.some(doc => doc.type === 'pdf' || doc.type === 'web'); + return docs.some(doc => doc.type === DocumentType.PDF || doc.type === DocumentType.WEB); } return false; }; @@ -339,18 +310,20 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.Status === 'marquee' ? ( <> {this.highlighter} - <Tooltip key="annotate" title={<div className="dash-tooltip">Drag to Place Annotation</div>}> - <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="comment-alt" size="lg" /> - </button> - </Tooltip> + <IconButton + tooltip="Drag to Place Annotation" // + onPointerDown={this.pointerDown} + icon={<FontAwesomeIcon icon="comment-alt" />} + color={StrCast(Doc.UserDoc().userColor)} + /> {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/} {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && ( - <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.gptSummarize} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="comment-dots" size="lg" /> - </button> - </Tooltip> + <IconButton + tooltip="Summarize with AI" // + onPointerDown={this.gptSummarize} + icon={<FontAwesomeIcon icon="comment-dots" size="lg" />} + color={StrCast(Doc.UserDoc().userColor)} + /> )} <GPTPopup key="gptpopup" @@ -364,56 +337,74 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { mode={this.GPTMode} /> {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( - <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">Click to Record Annotation</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="microphone" size="lg" /> - </button> - </Tooltip> + <IconButton + tooltip="Click to Record Annotation" // + onPointerDown={this.audioDown} + icon={<FontAwesomeIcon icon="microphone" />} + color={StrCast(Doc.UserDoc().userColor)} + /> )} {this.canEdit() && ( - <Tooltip key="gpttextedit" title={<div className="dash-tooltip">AI edit suggestions</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.gptEdit} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="pencil-alt" size="lg" /> - </button> - </Tooltip> + <IconButton + tooltip="AI edit suggestions" // + onPointerDown={this.gptEdit} + icon={<FontAwesomeIcon icon="pencil-alt" />} + color={StrCast(Doc.UserDoc().userColor)} + /> )} - <Tooltip key="link" title={<div className="dash-tooltip">Find document to link to selected text</div>}> - <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup}> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> - <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" /> - </button> - </Tooltip> - <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />, + <Popup + tooltip="Find document to link to selected text" // + type={Type.PRIM} + icon={<FontAwesomeIcon icon={'search'} />} + popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />} + color={StrCast(Doc.UserDoc().userColor)} + /> {AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : ( - <Tooltip key="crop" title={<div className="dash-tooltip">Click/Drag to create cropped image</div>}> - <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}> - <FontAwesomeIcon icon="image" size="lg" /> - </button> - </Tooltip> + <IconButton + tooltip="Click/Drag to create cropped image" // + onPointerDown={this.cropDown} + icon={<FontAwesomeIcon icon="image" />} + color={StrCast(Doc.UserDoc().userColor)} + /> )} </> ) : ( <> - <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}> - <button className="antimodeMenu-button" onPointerDown={this.Delete}> - <FontAwesomeIcon icon="trash-alt" size="lg" /> - </button> - </Tooltip> - <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}> - <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> - <FontAwesomeIcon icon="map-pin" size="lg" /> - </button> - </Tooltip> - <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}> - <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}> - <FontAwesomeIcon icon="taxi" size="lg" /> - </button> - </Tooltip> - <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}> - <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}> - <FontAwesomeIcon icon="thumbtack" size="lg" /> - </button> - </Tooltip> + {this.Delete !== returnFalse && ( + <IconButton + tooltip="Remove Link Anchor" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} + {this.PinToPres !== returnFalse && ( + <IconButton + tooltip="Pin to Presentation" // + onPointerDown={this.PinToPres} + icon={<FontAwesomeIcon icon="map-pin" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} + {this.ShowTargetTrail !== returnFalse && ( + <IconButton + tooltip="Show Linked Trail" // + onPointerDown={this.ShowTargetTrail} + icon={<FontAwesomeIcon icon="taxi" />} + color={StrCast(Doc.UserDoc().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={StrCast(Doc.UserDoc().userColor)} + /> + )} </> ); diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 470aa3eb1..cfe07f6cb 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -26,15 +26,9 @@ .textLayer { opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! - - // span { - // padding-right: 5px; - // padding-bottom: 4px; - // } } - .textLayer ::selection { - background: #accef7; + background: #accef76a; } // should match the backgroundColor in createAnnotation() diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 40b745cc4..1319a236d 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -2,7 +2,8 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; -import { Doc, DocListCast, Field, HeightSym, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { Height } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; @@ -25,6 +26,7 @@ import { Annotation } from './Annotation'; import './PDFViewer.scss'; import React = require('react'); import { GPTPopup } from './GPTPopup/GPTPopup'; +import { InkingStroke } from '../InkingStroke'; const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer'); const pdfjsLib = require('pdfjs-dist'); const _global = (window /* browser */ || global) /* node */ as any; @@ -90,7 +92,7 @@ export class PDFViewer extends React.Component<IViewerProps> { @observable isAnnotating = false; // key where data is stored @computed get allAnnotations() { - return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.docFilters(), this.props.docRangeFilters()); + return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.childFilters(), this.props.childFiltersByRanges()); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); @@ -181,7 +183,7 @@ export class PDFViewer extends React.Component<IViewerProps> { let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); + const scrollTo = Utils.scrollIntoView(scrollTop, doc[Height](), NumCast(this.props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._layout_scrollTop) { if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc }; else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper); @@ -482,16 +484,17 @@ export class PDFViewer extends React.Component<IViewerProps> { } }; - pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); + pointerEvents = () => + this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() + ? 'all' // + : 'none'; @computed get annotationLayer() { + const inlineAnnos = this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).filter(anno => !anno.hidden); return ( <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .filter(anno => !anno.hidden) - .map(anno => ( - <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> - ))} + {inlineAnnos.map(anno => ( + <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + ))} </div> ); } @@ -500,7 +503,7 @@ export class PDFViewer extends React.Component<IViewerProps> { return !this._overlayAnnoInfo ? null : ( <div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}> <div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}> - {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.creationDate as Field)} + {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.author_date as Field)} </div> </div> ); @@ -512,17 +515,18 @@ export class PDFViewer extends React.Component<IViewerProps> { overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._freeform_scale, 1)); panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return 'none'; - return 'all'; + if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none'; + const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; + return isInk ? 'visiblePainted' : 'all'; } return this.props.styleProvider?.(doc, props, property); }; - renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => ( + renderAnnotations = (childFilters: () => string[], mixBlendMode?: any, display?: string) => ( <div className="pdfViewerDash-overlay" style={{ @@ -535,8 +539,8 @@ export class PDFViewer extends React.Component<IViewerProps> { NativeWidth={returnZero} NativeHeight={returnZero} setContentView={emptyFunction} // override setContentView to do nothing - pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. - childPointerEvents="all" // but freeform children need to get events to allow text editing, etc + pointerEvents={this.props.isContentActive() && (SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. + childPointerEvents={this.props.isContentActive() !== false ? 'all' : 'none'} // but freeform children need to get events to allow text editing, etc renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.props.fieldKey + '_annotations'} @@ -548,8 +552,7 @@ export class PDFViewer extends React.Component<IViewerProps> { ScreenToLocalTransform={this.overlayTransform} isAnyChildContentActive={returnFalse} isAnnotationOverlayScrollable={true} - dropAction="embed" - docFilters={docFilters} + childFilters={childFilters} select={emptyFunction} bringToFront={emptyFunction} styleProvider={this.childStyleProvider} @@ -557,14 +560,15 @@ export class PDFViewer extends React.Component<IViewerProps> { </div> ); @computed get overlayTransparentAnnotations() { - return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined); + const transparentChildren = DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.transparentFilter(), []); + return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); } @computed get overlayLayer() { return ( - <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}> + <div style={{ pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : 'none' }}> {this.overlayTransparentAnnotations} {this.overlayOpaqueAnnotations} </div> diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index e8865b918..a439aea3e 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -13,12 +13,16 @@ .searchBox-bar { width: 100%; - height: 35px; + height: fit-content; display: flex; justify-content: center; align-items: center; background-color: none; padding: 5px; + top: 0px; + position: sticky; + overflow-y: scroll; + border-bottom: $standard-border; .searchBox-type { display: block; @@ -42,34 +46,66 @@ } } - .searchBox-results-container { + .section-header { + + .section-title { + font-size: $body-text; + font-weight: 600; + } + + .section-subtitle { + display: flex; + color: $light-gray; + } + + padding: 5px 10px; + display: flex; + flex-direction: column; + gap: 3px; + background: $medium-blue; + color: white; + } + + .searchBox-recommendations-container { display: flex; flex-direction: column; width: 100%; - height: 100%; + height: fit-content; justify-content: "center"; - - .searchBox-results-count { + + .searchBox-recommendations-view { + margin-top: 10px; display: flex; - color: gray; - margin-left: 5px; + width: 100%; + height: fit-content; + flex-direction: column; + gap: 10px; + padding: 0px 10px; + + } + } + + .searchBox-results-container { + display: flex; + flex-direction: column; + width: 100%; + height: fit-content; + justify-content: "center"; - .searchBox-results-scroll-view { - margin-top: 10px; + .searchBox-results-view { display: inline-block; width: 100%; - height: calc(100% - 55px); - overflow-y: scroll; + height: fit-content; .searchBox-results-scroll-view-result { display: inline-block; vertical-align: middle; width: 100%; - height: 50px; + height: fit-content; cursor: pointer; font-size: 15px; - padding: 11px; + padding: 10px; &.searchBox-results-scroll-view-result-selected { background: #999; @@ -81,6 +117,8 @@ width: calc(100% - 45px); text-align: left; overflow: hidden; + max-height: 2.4em; + line-height: 1.2em; text-overflow: ellipsis; } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 536ec7c75..1ceea697a 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -2,7 +2,8 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc'; +import { DirectLinks } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, StrCast } from '../../../fields/Types'; import { DocUtils } from '../../documents/Documents'; @@ -14,6 +15,10 @@ import { CollectionDockingView } from '../collections/CollectionDockingView'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './SearchBox.scss'; +import { fetchRecommendations } from '../newlightbox/utils'; +import { IRecommendation, Recommendation } from '../newlightbox/components'; +import { Colors } from '../global/globalEnums'; +import { SettingsManager } from '../../util/SettingsManager'; const DAMPENING_FACTOR = 0.9; const MAX_ITERATIONS = 25; @@ -42,6 +47,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @observable _searchString = ''; @observable _docTypeString = 'all'; @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>(); + @observable _recommendations: IRecommendation[] = []; @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>(); @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @@ -141,6 +147,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { 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; @@ -211,7 +219,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } @action static staticSearchCollection(rootDoc: Opt<Doc>, query: string) { - const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.MARKER, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; + const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.SEARCH, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = [ 'x', 'y', @@ -219,7 +227,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { 'width', 'layout_autoHeight', 'acl-Override', - 'acl-Public', + 'acl-Guest', 'embedContainer', 'zIndex', 'height', @@ -240,7 +248,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { 'layout', 'layout_keyValue', 'layout_fitWidth', - 'viewType', + 'type_collection', 'title_custom', 'freeform_panX', 'freeform_panY', @@ -290,7 +298,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { this._results.forEach((_, doc) => { this._pageRanks.set(doc, 1.0 / this._results.size); - if (Doc.GetProto(doc)[DirectLinksSym].size === 0) { + if (Doc.GetProto(doc)[DirectLinks].size === 0) { this._linkedDocsOut.set(doc, new Set(this._results.keys())); this._results.forEach((_, linkedDoc) => { @@ -299,7 +307,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } else { const linkedDocSet = new Set<Doc>(); - Doc.GetProto(doc)[DirectLinksSym].forEach(link => { + Doc.GetProto(doc)[DirectLinks].forEach(link => { const d1 = link?.link_anchor_1 as Doc; const d2 = link?.link_anchor_2 as Doc; if (doc === d1 && this._results.has(d2)) { @@ -391,6 +399,37 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { if (query) { this.searchCollection(query); + const response = await fetchRecommendations('', query, [], true); + const recs = response.recommendations; + const recommendations: IRecommendation[] = []; + for (const key in recs) { + const title = recs[key].title; + const url = recs[key].url; + const type = recs[key].type; + const text = recs[key].text; + const transcript = recs[key].transcript; + const previewUrl = recs[key].previewUrl; + const embedding = recs[key].embedding; + const distance = recs[key].distance; + const source = recs[key].source; + const related_concepts = recs[key].related_concepts; + const docId = recs[key].doc_id; + recommendations.push({ + title: title, + data: url, + type: type, + text: text, + transcript: transcript, + previewUrl: previewUrl, + embedding: embedding, + distance: Math.round(distance * 100) / 100, + source: source, + related_concepts: related_concepts, + docId: docId, + }); + } + const setRecommendations = action(() => (this._recommendations = recommendations)); + setRecommendations(); } }; @@ -400,6 +439,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { */ resetSearch = action(() => { this._results.forEach((_, doc) => { + DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); Doc.UnBrushDoc(doc); Doc.UnHighlightDoc(doc); Doc.ClearSearchMatches(); @@ -422,7 +462,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { */ @computed public get selectOptions() { - const selectValues = ['all', 'rtf', 'image', 'pdf', 'web', 'video', 'audio', 'collection']; + const selectValues = ['all', DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.WEB, DocumentType.VID, DocumentType.AUDIO, DocumentType.COL]; return selectValues.map(value => ( <option key={value} value={value}> @@ -478,15 +518,19 @@ 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">{result[1].join(', ')}</div> + <div className="searchBox-result-keys" style={{ color: SettingsManager.Instance.userVariantColor }}> + {result[1].join(', ')} + </div> </div> </Tooltip> ); } }); + const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />); + return ( - <div style={{ pointerEvents: 'all' }} className="searchBox-container"> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.Instance.userColor, background: SettingsManager.Instance.userBackgroundColor }}> <div className="searchBox-bar"> {isLinkSearch ? null : ( <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}> @@ -509,10 +553,24 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { ref={this._inputRef} /> </div> - <div className="searchBox-results-container"> - <div className="searchBox-results-count">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> - <div className="searchBox-results-scroll-view">{resultsJSX}</div> - </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-title">Results</div> + <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + </div> + <div className="searchBox-results-view">{resultsJSX}</div> + </div> + )} + {recommendationsJSX.length > 0 && ( + <div className="searchBox-recommendations-container"> + <div className="section-header" style={{ background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE) }}> + <div className="section-title">Recommendations</div> + <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div> + </div> + <div className="searchBox-recommendations-view">{recommendationsJSX}</div> + </div> + )} </div> ); } diff --git a/src/client/views/selectedDoc/SelectedDocView.scss b/src/client/views/selectedDoc/SelectedDocView.scss new file mode 100644 index 000000000..156dfc37b --- /dev/null +++ b/src/client/views/selectedDoc/SelectedDocView.scss @@ -0,0 +1,3 @@ +.selectedDocView-container { + +}
\ No newline at end of file diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx new file mode 100644 index 000000000..955a4a174 --- /dev/null +++ b/src/client/views/selectedDoc/SelectedDocView.tsx @@ -0,0 +1,47 @@ +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 { DocumentManager } from '../../util/DocumentManager'; +import { DocFocusOptions } from '../nodes/DocumentView'; + +export interface SelectedDocViewProps { + selectedDocs: Doc[]; +} + +@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> + } +}
\ No newline at end of file diff --git a/src/client/views/selectedDoc/index.ts b/src/client/views/selectedDoc/index.ts new file mode 100644 index 000000000..1f1db91f6 --- /dev/null +++ b/src/client/views/selectedDoc/index.ts @@ -0,0 +1 @@ +export * from './SelectedDocView'
\ No newline at end of file diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index a1131b92e..2237d5ac1 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -1,243 +1,240 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .topbar-container { - flex-direction: column; - font-size: 10px; - line-height: 1; - overflow-y: auto; - overflow-x: visible; - background: $dark-gray; - overflow: visible; - z-index: 1000; - align-items: center; - height: $topbar-height; - background-color: $dark-gray; - border-bottom: $standard-border; - padding: 0px 10px; - cursor: default; - display: flex; - justify-content: center; + flex-direction: column; + font-size: 10px; + line-height: 1; + overflow-y: auto; + overflow-x: visible; + background: $dark-gray; + overflow: visible; + z-index: 1000; + align-items: center; + height: $topbar-height; + background-color: $dark-gray; + border-bottom: $standard-border; + padding: 0px 10px; + cursor: default; + display: flex; + justify-content: center; + width: 100%; + + .topbar-inner-container { + display: flex; + flex-direction: row; + position: relative; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; width: 100%; + align-items: center; - .topbar-inner-container { - display: flex; - flex-direction: row; - position: relative; - display: grid; - grid-auto-columns: 33.3% 33.3% 33.3%; - width: 100%; - align-items: center; - - // &:first-child { - // height: 20px; - // } - } + // &:first-child { + // height: 20px; + // } + } - .topbar-button-text { - color: $white; - padding: 10px; - size: 15; + .topbar-button-text { + color: $white; + padding: 10px; + size: 15; - &:hover { - font-weight: 500; - } - } + &:hover { + font-weight: 500; + } + } - .topbar-button-icon { - cursor: pointer; - width: fit-content; + .topbar-button-icon { + cursor: pointer; + width: fit-content; + display: flex; + justify-content: center; + gap: 4px; + align-items: center; + justify-self: center; + align-self: center; + padding: 5px; + transition: linear 0.2s; + color: $white; + + &:hover { + background-color: darken($color: $light-gray, $amount: 20); + font-weight: 500; + } + } + + .topbar-title { + color: $white; + font-size: 17; + font-weight: 500; + } + + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; + align-items: center; + gap: 5px; + + .topbar-dashboard-header { + font-weight: 600; + } + } + + .topbar-right { + grid-column: 3; + position: relative; + display: flex; + justify-content: flex-end; + gap: 5px; + margin-right: 5px; + } + + .topbar-left { + grid-column: 1; + color: black; + font-family: 'Roboto'; + position: relative; + display: flex; + width: fit-content; + gap: 5px; + + .logo-container { + font-size: 15; display: flex; + flex-direction: row; justify-content: center; - gap: 4px; align-items: center; - justify-self: center; - align-self: center; - padding: 5px; - transition: linear 0.2s; - color: $white; - - &:hover { - background-color: darken($color: $light-gray, $amount: 20); - font-weight: 500; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; + + .logo { + background-color: transparent; + width: 25px; + height: 25px; + margin-right: 5px; } - } + } - .topbar-title { - color: $white; - font-size: 17; - font-weight: 500; - } - - .topbar-center { - grid-column: 2; - display: inline-flex; - justify-content: center; - align-items: center; - gap: 5px; - - .topbar-dashboard-header { - font-weight: 600; - } - } + .topBar-icon:hover { + background-color: $close-red; + } - - .topbar-right { - grid-column: 3; - position: relative; - display: flex; - justify-content: flex-end; - gap: 5px; - margin-right: 5px; - } - - .topbar-left { - grid-column: 1; - color: black; + .topbar-lozenge-user, + .topbar-lozenge { + height: 23; + font-size: 12; + color: white; font-family: 'Roboto'; - position: relative; + font-weight: 400; + padding: 4px; + align-self: center; + margin-left: 7px; display: flex; - width: fit-content; - gap: 5px; - - .logo-container { - font-size: 15; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; - - .logo { - background-color: transparent; - width: 25px; - height: 25px; - margin-right: 5px; - - } - } + align-items: center; - .topBar-icon:hover { - background-color: $close-red; - } + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; - .topbar-lozenge-user, - .topbar-lozenge { - height: 23; - font-size: 12; - color: white; - font-family: 'Roboto'; - font-weight: 400; - padding: 4px; - align-self: center; - margin-left: 7px; - display: flex; - align-items: center; - - .topbar-dashSelect { - border: none; - background-color: transparent; - color: black; - font-family: 'Roboto'; - font-size: 17; - font-weight: 500; - - &:hover { - cursor: pointer; - } - } + &:hover { + cursor: pointer; + } } + } + + .topbar-logoff { + border-radius: 3px; + background: olivedrab; + color: white; + display: none; + margin-left: 5px; + padding: 1px 2px 1px 2px; + cursor: pointer; + } - .topbar-logoff { - border-radius: 3px; - background: olivedrab; - color: white; - display: none; - margin-left: 5px; - padding: 1px 2px 1px 2px; - cursor: pointer; - } + .topbar-logoff { + background: red; + } + .topbar-lozenge-user:hover { .topbar-logoff { - background: red; + display: inline-block; } - - .topbar-lozenge-user:hover { - .topbar-logoff { - display: inline-block; - } - } - } - - .topbar-barChild { - - &.topbar-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px - } - - &.topbar-input { - margin: 5px; - border-radius: 20px; - border: $dark-gray; - display: block; - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - /* align-self: stretch; */ - outline: none; - - &:focus { - width: 500px; - outline: none; - } + } + } + + .topbar-barChild { + &.topbar-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px; + } + + &.topbar-input { + margin: 5px; + border-radius: 20px; + border: $dark-gray; + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + /* align-self: stretch; */ + outline: none; + + &:focus { + width: 500px; + outline: none; } + } - &.topbar-filter { - align-self: stretch; + &.topbar-filter { + align-self: stretch; - button { - transform: none; + button { + transform: none; - &:hover { - transform: none; - } - } + &:hover { + transform: none; + } } + } - &.topbar-submit { - margin-left: 2px; - margin-right: 2px - } + &.topbar-submit { + margin-left: 2px; + margin-right: 2px; + } - &.topbar-close { - color: $white; - max-height: $topbar-height; - } - } + &.topbar-close { + color: $white; + max-height: $topbar-height; + } + } } .topbar-results { - display: flex; - flex-direction: column; - top: 300px; - display: flex; - flex-direction: column; - height: 100%; - overflow: visible; - - .no-result { - width: 500px; - background: $light-gray; - padding: 10px; - height: 50px; - text-transform: uppercase; - text-align: left; - font-weight: bold; - } -}
\ No newline at end of file + display: flex; + flex-direction: column; + top: 300px; + display: flex; + flex-direction: column; + height: 100%; + overflow: visible; + + .no-result { + width: 500px; + background: $light-gray; + padding: 10px; + height: 50px; + text-transform: uppercase; + text-align: left; + font-weight: bold; + } +} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 9bd2ba5ce..c194ede32 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,24 +1,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Size } from 'browndash-components'; -import { action, computed, observable } from 'mobx'; +import { Button, IconButton, Size, Type, isDark } 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 { AclAdmin, Doc } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; import { DocumentManager } from '../../util/DocumentManager'; -import { ReportManager } from '../../util/ReportManager'; +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 { CollectionDockingView } from '../collections/CollectionDockingView'; import { ContextMenu } from '../ContextMenu'; import { DashboardView } from '../DashboardView'; -import { Colors } from '../global/globalEnums'; import { MainView } from '../MainView'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; +import { Colors } from '../global/globalEnums'; 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 @@ -33,8 +36,22 @@ export class TopBar extends React.Component { }); }; - @observable textColor: string = Colors.LIGHT_GRAY; - @observable backgroundColor: string = Colors.DARK_GRAY; + @computed get color() { + return StrCast(Doc.UserDoc().userColor, Colors.LIGHT_GRAY); + } + @computed get variantColor() { + return StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE); + } + @computed get backgroundColor() { + return PingManager.Instance.IsBeating ? SettingsManager.Instance.userBackgroundColor : Colors.MEDIUM_GRAY; + } + + @observable happyHeart: boolean = PingManager.Instance.IsBeating; + setHappyHeart = action((status: boolean) => (this.happyHeart = status)); + dispose = reaction( + () => PingManager.Instance.IsBeating, + isBeating => this.setHappyHeart(isBeating) + ); /** * Returns the left hand side of the topbar. @@ -47,16 +64,20 @@ export class TopBar extends React.Component { return ( <div className="topbar-left"> {Doc.ActiveDashboard ? ( - <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} color={this.textColor} /> + <IconButton + onClick={this.navigateToHome} + icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />} + color={this.color} + /> ) : ( <div className="logo-container"> <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img> - <span style={{ color: Colors.LIGHT_GRAY, fontWeight: 200 }}>brown</span> - <span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span> + <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, fontWeight: 200 }}>brown</span> + <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_BLUE : Colors.MEDIUM_BLUE, fontWeight: 500 }}>dash</span> </div> )} {Doc.ActiveDashboard && ( - <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.textColor} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} /> + <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))} /> )} </div> ); @@ -83,36 +104,21 @@ export class TopBar extends React.Component { <div className="topbar-center"> <Button text={StrCast(Doc.ActiveDashboard.title)} - tooltip="Browsing mode for directly navigating to documents" + tooltip="Open Dashboards" size={Size.SMALL} - color={'white'} + color={this.color} + 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' }); - ContextMenu.Instance.addItem({ - description: 'Snapshot Dashboard', - event: async () => { - const batch = UndoManager.StartBatch('snapshot'); - await DashboardView.snapshotDashboard(); - batch.end(); - }, - icon: 'edit', - }); dashView?.showContextMenu(e.clientX + 20, e.clientY + 30); }} /> - <Button - text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'} - onClick={() => { - SharingManager.Instance.open(undefined, Doc.ActiveDashboard); - }} - size={Size.SMALL} - /> {!Doc.noviceMode && ( <IconButton tooltip="Work on a copy of the dashboard layout" size={Size.SMALL} - color={this.textColor} + color={this.color} onClick={async () => { const batch = UndoManager.StartBatch('snapshot'); await DashboardView.snapshotDashboard(); @@ -131,13 +137,31 @@ export class TopBar extends React.Component { * and allows the user to access their account settings etc. */ @computed get topbarRight() { + const upToDate = DashVersion === CurrentUserUtils.ServerVersion; return ( <div className="topbar-right"> - <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ServerStats.Instance.open} icon={<FaStamp />} /> - <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ReportManager.Instance.open} icon={<FaBug />} /> - <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> - <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} /> - {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.textColor} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */} + {Doc.ActiveDashboard ? ( + <Button + text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'} + type={Type.TERT} + color={this.variantColor} + onClick={() => { + SharingManager.Instance.open(undefined, Doc.ActiveDashboard); + }} + /> + ) : null} + <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> + <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> + <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} /> + <IconButton + size={Size.SMALL} + onClick={ServerStats.Instance.open} + type={Type.TERT} + tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running ' + (upToDate ? DashVersion : 'out of date version:' + DashVersion)} + color={this.happyHeart ? (upToDate ? Colors.LIGHT_BLUE : Colors.YELLOW) : Colors.ERROR_RED} + icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />} + /> + {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.color} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */} </div> ); } @@ -145,13 +169,15 @@ export class TopBar extends React.Component { render() { return ( //TODO:glr Add support for light / dark mode - <div style={{ pointerEvents: 'all' }} className="topbar-container"> - <div - className="topbar-inner-container" - style={{ - color: this.textColor, - background: this.backgroundColor, - }}> + <div + style={{ + pointerEvents: 'all', + color: this.color, + background: this.backgroundColor, + // borderColor: this.color + }} + className="topbar-container"> + <div className="topbar-inner-container"> {this.topbarLeft} {this.topbarCenter} {this.topbarRight} |
