diff options
Diffstat (limited to 'src/client/views')
56 files changed, 1431 insertions, 1657 deletions
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 7ebe8d0e3..2b586b0e2 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -233,7 +233,6 @@ export class DashboardView extends React.Component { /// 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) => { if (!doc) return false; - Doc.MainDocId = doc[Id]; Doc.AddDocToList(Doc.MyDashboards, 'data', doc); // this has the side-effect of setting the main container since we're assigning the active/guest dashboard diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 7c81d92d4..0b92fd864 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -16,6 +16,7 @@ import { Touchable } from './Touchable'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) export interface DocComponentProps { Document: Doc; + fieldKey?: string; LayoutTemplate?: () => Opt<Doc>; LayoutTemplateString?: string; } @@ -37,6 +38,10 @@ export function DocComponent<P extends DocComponentProps>() { @computed get dataDoc() { return this.props.Document[DataSym] as Doc; } + // key where data is stored + @computed get fieldKey() { + return this.props.fieldKey; + } protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index af868ba9c..9389fed01 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -518,7 +518,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ) }> <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}> - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" /> </div> </Flyout> </div> diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss new file mode 100644 index 000000000..7f907c8d4 --- /dev/null +++ b/src/client/views/FilterPanel.scss @@ -0,0 +1,189 @@ +.filterBox-flyout { + display: block; + text-align: left; + font-weight: 100; + + .filterBox-flyout-facet { + background-color: white; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + + .filterBox-flyout-facet-check { + margin-right: 6px; + } + } +} + +.filter-bookmark { + //display: flex; + + .filter-bookmark-icon { + float: right; + margin-right: 10px; + margin-top: 7px; + } +} + +// .filterBox-bottom { +// // position: fixed; +// // bottom: 0; +// // width: 100%; +// } + +.filterBox-select { + // width: 90%; + margin-top: 5px; + // margin-bottom: 15px; +} + +.filterBox-saveBookmark { + background-color: #e9e9e9; + border-radius: 11px; + padding-left: 8px; + padding-right: 8px; + padding-top: 5px; + padding-bottom: 5px; + margin: 8px; + display: flex; + font-size: 11px; + cursor: pointer; + + &:hover { + background-color: white; + } + + .filterBox-saveBookmark-icon { + margin-right: 6px; + margin-top: 4px; + margin-left: 2px; + } +} + +.filterBox-select-scope, +.filterBox-select-bool, +.filterBox-addWrapper, +.filterBox-select-matched, +.filterBox-saveWrapper { + font-size: 10px; + justify-content: center; + justify-items: center; + padding-bottom: 10px; + display: flex; +} + +.filterBox-addWrapper { + font-size: 11px; + width: 100%; +} + +.filterBox-saveWrapper { + width: 100%; +} + +// .filterBox-top { +// padding-bottom: 20px; +// border-bottom: 2px solid black; +// position: fixed; +// top: 0; +// width: 100%; +// } + +.filterBox-select-scope { + padding-bottom: 20px; + border-bottom: 2px solid black; +} + +.filterBox-title { + font-size: 15; + // border: 2px solid black; + width: 100%; + align-self: center; + text-align: center; + background-color: #d3d3d3; +} + +.filterBox-select-bool { + margin-top: 6px; +} + +.filterBox-select-text { + margin-right: 8px; + margin-left: 8px; + margin-top: 3px; +} + +.filterBox-select-box { + margin-right: 2px; + font-size: 30px; + border: 0; + background: transparent; +} + +.filterBox-selection { + border-radius: 6px; + border: none; + background-color: #e9e9e9; + padding: 2px; + + &:hover { + background-color: white; + } +} + +.filterBox-addFilter { + width: 120px; + background-color: #e9e9e9; + border-radius: 12px; + padding: 5px; + margin: 5px; + display: flex; + text-align: center; + justify-content: center; + + &:hover { + background-color: white; + } +} + +.filterBox-treeView { + display: flex; + flex-direction: column; + width: 200px; + position: absolute; + right: 0; + top: 0; + z-index: 1; + background-color: #9f9f9f; + + .filterBox-tree { + z-index: 0; + } + + .filterBox-addfacet { + display: inline-block; + width: 200px; + height: 30px; + text-align: left; + + .filterBox-addFacetButton { + display: flex; + margin: auto; + cursor: pointer; + } + + > div, + > div > div { + width: 100%; + height: 100%; + } + } + + .filterBox-tree { + display: inline-block; + width: 100%; + margin-bottom: 10px; + //height: calc(100% - 30px); + } +} diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx new file mode 100644 index 000000000..d35494f26 --- /dev/null +++ b/src/client/views/FilterPanel.tsx @@ -0,0 +1,232 @@ +import React = require('react'); +import { action, computed, observable, ObservableMap } from 'mobx'; +import { observer } from 'mobx-react'; +import Select from 'react-select'; +import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc'; +import { RichTextField } from '../../fields/RichTextField'; +import { StrCast } from '../../fields/Types'; +import { DocumentManager } from '../util/DocumentManager'; +import { UserOptions } from '../util/GroupManager'; +import './FilterPanel.scss'; +import { FieldView } from './nodes/FieldView'; +import { SearchBox } from './search/SearchBox'; + +interface filterProps { + rootDoc: Doc; +} +@observer +export class FilterPanel extends React.Component<filterProps> { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FilterPanel, fieldKey); + } + + /** + * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection + */ + @computed get targetDoc() { + return this.props.rootDoc; + } + @computed get targetDocChildKey() { + const targetView = DocumentManager.Instance.getFirstDocumentView(this.targetDoc); + return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data'; + } + @computed get targetDocChildren() { + return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data); + } + + @computed get allDocs() { + const allDocs = new Set<Doc>(); + const targetDoc = this.targetDoc; + if (targetDoc) { + SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); + } + return Array.from(allDocs); + } + + @computed get _allFacets() { + // trace(); + const noviceReqFields = ['author', 'tags', 'text', 'type']; + const noviceLayoutFields: string[] = []; //["_curPage"]; + const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; + + const keys = new Set<string>(noviceFields); + this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); + return Array.from(keys.keys()) + .filter(key => key[0]) + .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) + .sort(); + } + + /** + * The current attributes selected to filter based on + */ + @computed get activeFilters() { + return StrListCast(this.targetDoc?._docFilters); + } + + /** + * @returns a string array of the current attributes + */ + @computed get currentFacets() { + return this.activeFilters.map(filter => filter.split(':')[0]); + } + + gatherFieldValues(childDocs: Doc[], facetKey: string) { + const valueSet = new Set<string>(); + let rtFields = 0; + let subDocs = childDocs; + if (subDocs.length > 0) { + let newarray: Doc[] = []; + while (subDocs.length > 0) { + newarray = []; + subDocs.forEach(t => { + const facetVal = t[facetKey]; + if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; + facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); + (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal)); + 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)); + }); + subDocs = newarray; + } + } + // } + // }); + return { strings: Array.from(valueSet.keys()), rtFields }; + } + + public removeFilter = (filterName: string) => { + Doc.setDocFilter(this.targetDoc, filterName, undefined, 'remove'); + Doc.setDocRangeFilter(this.targetDoc, filterName, undefined); + }; + + @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); + @computed get activeFacets() { + const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets); + StrListCast(this.targetDoc?._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')))); + return facets; + } + /** + * Responds to clicking the check box in the flyout menu + */ + @action + facetClick = (facetHeader: string) => { + if (!this.targetDoc) return; + const allCollectionDocs = this.targetDocChildren; + const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, + maxVal = -Number.MAX_VALUE; + facetValues.strings.map(val => { + const num = val ? Number(val) : Number.NaN; + if (Number.isNaN(num)) { + val && nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.rtFields > 20)) { + this._chosenFacets.set(facetHeader, 'text'); + } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { + } else { + this._chosenFacets.set(facetHeader, 'checkbox'); + } + }; + + facetValues = (facetHeader: string) => { + const allCollectionDocs = new Set<Doc>(); + SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); + const set = new Set<string>(); + if (facetHeader === 'tags') + allCollectionDocs.forEach(child => + Field.toString(child[facetHeader] as Field) + .split(':') + .forEach(key => set.add(key)) + ); + else + allCollectionDocs.forEach(child => { + const fieldVal = child[facetHeader] as Field; + set.add(Field.toString(fieldVal)); + (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); + }); + const facetValues = Array.from(set).filter(v => v); + + let nonNumbers = 0; + + facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); + const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { + return facetValue; + }); + return facetValueDocSet; + }; + + render() { + const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); + return ( + <div className="filterBox-treeView" style={{ position: 'relative', width: '100%' }}> + <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)}> + {['AND', 'OR'].map(bool => ( + <option value={bool} key={bool}> + {bool} + </option> + ))} + </select> + <div className="filterBox-select-text">filters together</div> + </div> + + <div className="filterBox-select"> + <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-tree" key="tree" style={{ overflow: 'auto' }}> + {Array.from(this.activeFacets.keys()).map(facetHeader => ( + <div> + {facetHeader} + {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)} + </div> + ))} + </div> + </div> + ); + } + + private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode { + switch (type) { + case 'text': + return ( + <input + placeholder={ + StrListCast(this.targetDoc._docFilters) + .find(filter => filter.split(':')[0] === facetHeader) + ?.split(':')[1] ?? '-empty-' + } + onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')} + /> + ); + case 'checkbox': + return this.facetValues(facetHeader).map(fval => { + const facetValue = fval; + return ( + <div> + <input + style={{ width: 20, marginLeft: 20 }} + checked={ + StrListCast(this.targetDoc._docFilters) + .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue) + ?.split(':')[2] === 'check' + } + type={type} + onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')} + /> + {facetValue} + </div> + ); + }); + } + } +} diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 6058eaaf9..0feccb742 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -94,7 +94,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { } static setupThumbButtons(doc: Doc) { - const docProtoData: { title: string; icon: string; drag?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: 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' }, { @@ -105,8 +105,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }), backgroundColor: 'orange', }, - { title: 'interpret text', icon: 'font', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: 'orange' }, - { title: 'ignore gestures', icon: 'signature', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: 'green' }, + { 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({ @@ -116,6 +116,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { _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, diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 3fbf6e445..0f8b46dbe 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -372,7 +372,7 @@ export class KeyManager { 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)).clone : d))); + 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.context = targetDataDoc)); undoBatch(() => { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 4f08a8e22..3861331b5 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -418,7 +418,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { inkScaleX, inkScaleY, '', - this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'), + this.props.pointerEvents?.() ?? 'visiblepainted', 0.0, false, downHdlr, diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 2567d44bb..976c8763e 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -4,9 +4,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { InkTool } from '../../fields/InkField'; -import { List } from '../../fields/List'; -import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; +import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; @@ -88,7 +87,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { ...future .slice() .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)) - .sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length), + .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length), ]; } this._doc = doc; @@ -214,7 +213,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { if (coll) { const fieldKey = Doc.LayoutFieldKey(coll); const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])]; - const links = DocListCast(coll.links) + const links = LinkManager.Links(coll) .map(link => LinkManager.getOppositeAnchor(link, coll)) .filter(doc => doc) .map(doc => doc!); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e1ba5943d..e755f204b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -10,7 +10,7 @@ import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; -import { DocCast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -57,7 +57,6 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; -import { PresBox } from './nodes/trails'; import { OverlayView } from './OverlayView'; import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; @@ -230,8 +229,6 @@ export class MainView extends React.Component { if (window.location.pathname !== '/home') { const pathname = window.location.pathname.substr(1).split('/'); if (pathname.length > 1 && pathname[0] === 'doc') { - Doc.MainDocId = pathname[1]; - //!this.userDoc && DocServer.GetRefField(pathname[1]).then( action(field => { if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) { @@ -480,9 +477,12 @@ export class MainView extends React.Component { fa.faHighlighter, fa.faRemoveFormat, fa.faHandPointUp, + fa.faXRay, + fa.faZ, + fa.faArrowsUpToLine, + fa.faArrowsDownToLine, ] ); - this.initAuthenticationRouters(); } globalPointerDown = action((e: PointerEvent) => { @@ -521,23 +521,6 @@ export class MainView extends React.Component { document.oncontextmenu = () => false; }; - initAuthenticationRouters = async () => { - const received = Doc.MainDocId; - if (received && !this.userDoc) { - reaction( - () => Doc.GuestTarget, - target => target && DashboardView.createNewDashboard(), - { fireImmediately: true } - ); - } - // else { - // PromiseValue(this.userDoc.activeDashboard).then(dash => { - // if (dash instanceof Doc) DashboardView.openDashboard(dash); - // else Doc.createNewDashboard(); - // }); - // } - }; - @action createNewPresentation = () => { const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); @@ -553,8 +536,6 @@ export class MainView extends React.Component { Doc.MyTrails && (Doc.ActivePresentation = pres); Doc.AddDocToList(Doc.MyTrails, 'data', pres); this.closeFlyout(); - } else { - PresBox.NavigateToDoc(DocCast(pres.presentationTargetDoc), pres); } }; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 08285ff0c..34e8cd6dd 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -255,4 +255,5 @@ export class OverlayView extends React.Component { // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) { OverlayView.Instance.addWindow(<ScriptingRepl />, options); + addOverlayWindow('ScriptingRepl', { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); }); diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 3712fff58..95ae65d7a 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -81,14 +81,8 @@ export class PreviewCursor extends React.Component<{}> { const batch = UndoManager.StartBatch('cloning'); { - const docs = await Promise.all( - docids - .filter((did, i) => i) - .map(async did => { - const doc = Cast(await DocServer.GetRefField(did), Doc, null); - return clone ? (await Doc.MakeClone(doc)).clone : doc; - }) - ); + 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 => { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index ac1b66013..11e9dd9c9 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -9,8 +9,10 @@ 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 { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { undoBatch } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; @@ -19,8 +21,6 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { pasteImageBitmap } from './nodes/WebBoxRenderer'; import './PropertiesButtons.scss'; import React = require('react'); -import { LinkManager } from '../util/LinkManager'; -import { Utils } from '../../Utils'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -133,15 +133,13 @@ export class PropertiesButtons extends React.Component<{}, {}> { const containerDoc = dv.rootDoc; //containerDoc.followAllLinks = // containerDoc.noShadow = - // containerDoc.noHighlighting = + // containerDoc.disableDocBrushing = // containerDoc._forceActive = containerDoc._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 => { - DocListCast(doc.links).forEach(link => (link.linkDisplay = false)); - }); + containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.linkDisplay = false))); }); } ); @@ -330,7 +328,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { const click = () => this.handleOptionChange(value[0]); const linkButton = BoolCast(this.selectedDoc._isLinkButton); const followLoc = this.selectedDoc._followLinkLocation; - const linkedToLightboxView = () => DocListCast(this.selectedDoc.links).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); + const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); let active = false; // prettier-ignore diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index e58558aea..dfc43e6c8 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -4,10 +4,10 @@ import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-soli import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; import { intersection } from 'lodash'; -import { action, autorun, computed, Lambda, observable } from 'mobx'; +import { action, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym, DocCastAsync } from '../../fields/Doc'; +import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -23,10 +23,10 @@ import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } 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 { FilterBox } from './nodes/FilterBox'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; @@ -76,13 +76,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openOptions: boolean = true; @observable openSharing: boolean = true; - @observable openFields: boolean = true; + @observable openFields: boolean = false; @observable openLayout: boolean = false; @observable openContexts: boolean = true; @observable openLinks: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; - @observable openFilters: boolean = true; // should be false + @observable openFilters: boolean = false; /** * autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open @@ -94,7 +94,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable layoutDocAcls: boolean = false; //Pres Trails booleans: - @observable openPresTransitions: boolean = false; + @observable openPresTransitions: boolean = true; + @observable openPresProgressivize: boolean = false; + @observable openPresVisibilityAndDuration: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; @@ -104,7 +106,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { componentDidMount() { this.selectedDocListenerDisposer?.(); - this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); + // this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); } componentWillUnmount() { @@ -1192,37 +1194,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } /** - * Checks if a currentFilter (FilterDoc) exists on the current collection (if the Properties Panel + Filters submenu are open). - * If it doesn't exist, it creates it. - */ - checkFilterDoc() { - if (!this.selectedDoc?.currentFilter) this.selectedDoc!.currentFilter = FilterBox.createFilterDoc(); - } - - /** - * Creates a new currentFilter for this.filterDoc, - */ - createNewFilterDoc = () => { - if (this.selectedDoc) { - const curFilterDoc = DocCast(this.selectedDoc.currentFilter); - const currentDocFilters = this.selectedDoc._docFilters; - const currentDocRangeFilters = this.selectedDoc._docRangeFilters; - this.selectedDoc._docFilters = new List<string>(); - this.selectedDoc._docRangeFilters = new List<string>(); - if (DocListCast(Doc.UserDoc().savedFilters).includes(curFilterDoc)) { - curFilterDoc._docFiltersList = currentDocFilters; - curFilterDoc._docRangeFiltersList = currentDocRangeFilters; - this.selectedDoc.currentFilter = FilterBox.createFilterDoc(); - } else { - Doc.GetProto(curFilterDoc).data = undefined; - Doc.GetProto(curFilterDoc).title = 'Unnamed Filter'; - curFilterDoc._docFiltersList = undefined; - curFilterDoc._docRangeFiltersList = undefined; - } - } - }; - - /** * Updates this.filterDoc's currentFilter and saves the docFilters on the currentFilter */ updateFilterDoc = (doc: Doc) => { @@ -1247,7 +1218,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }; @computed get filtersSubMenu() { - return !(this.selectedDoc?.currentFilter instanceof Doc) ? null : ( + return ( <div className="propertiesView-filters"> <div className="propertiesView-filters-title" onPointerDown={action(() => (this.openFilters = !this.openFilters))} style={{ backgroundColor: this.openFilters ? 'black' : '' }}> Filters @@ -1256,35 +1227,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> </div> {!this.openFilters ? null : ( - <div className="propertiesView-filters-content" style={{ height: this.selectedDoc.currentFilter[HeightSym]() + 15 }}> - <DocumentView - Document={this.selectedDoc.currentFilter} - DataDoc={undefined} - addDocument={undefined} - addDocTab={returnFalse} - pinToPres={emptyFunction} - rootSelected={returnTrue} - removeDocument={returnFalse} - ScreenToLocalTransform={this.getTransform} - PanelWidth={() => this.props.width} - PanelHeight={this.selectedDoc.currentFilter[HeightSym]} - renderDepth={0} - scriptContext={this.selectedDoc.currentFilter} - focus={emptyFunction} - styleProvider={DefaultStyleProvider} - isContentActive={returnTrue} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - createNewFilterDoc={this.createNewFilterDoc} - updateFilterDoc={this.updateFilterDoc} - docViewPath={returnEmptyDoclist} - dontCenter="y" - /> + <div className="propertiesView-filters-content" style={{ position: 'relative', height: 'auto' }}> + <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} /> </div> )} </div> @@ -1598,7 +1542,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3)); const targZoom = this.sourceAnchor?.followLinkZoom; const indent = 30; - const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)); + const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!); if (!this.selectedDoc && !this.isPres) { return ( <div className="propertiesView" style={{ width: this.props.width }}> @@ -1790,8 +1734,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline"> <p>Center Target (no zoom)</p> <button - style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))} + 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" /> @@ -1799,7 +1743,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </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 || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'inline-flex' }}> + <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))}> @@ -1818,11 +1762,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> </button> </div> - {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} + {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} <div className={'slider-headers'} style={{ - display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid', + display: !targZoom ? 'none' : 'grid', justifyContent: 'space-between', width: `calc(100% - ${indent * 2}px)`, marginLeft: indent, @@ -1854,7 +1798,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } if (this.isPres) { const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0; - const type = PresBox.Instance.activeItem?.type; + 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 }}> @@ -1878,6 +1824,31 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null} </div> )} + {!selectedItem ? null : ( + <div className="propertiesView-presTrails"> + <div + className="propertiesView-presTrails-title" + onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))} + 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" /> + </div> + </div> + {this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null} + </div> + )} + {!selectedItem ? null : ( + <div className="propertiesView-presTrails"> + <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" /> + </div> + </div> + {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null} + </div> + )} {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : ( <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}> diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 74ea624a6..02dd15960 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -3,10 +3,12 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; +import { RichTextField } from '../../fields/RichTextField'; import { DocCast, NumCast, StrCast } from '../../fields/Types'; import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { LinkManager } from '../util/LinkManager'; import { Transform } from '../util/Transform'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { FieldViewProps } from './nodes/FieldView'; @@ -15,7 +17,6 @@ import { SearchBox } from './search/SearchBox'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProvider'; import React = require('react'); -import { RichTextField } from '../../fields/RichTextField'; interface ExtraProps { fieldKey: string; @@ -83,7 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { Doc.GetProto(target)[key] = val; return { type: 'dashField', - attrs: { fieldKey: key, docid: '', hideKey: false, editable: true }, + attrs: { fieldKey: key, docId: '', hideKey: false, editable: true }, marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }], }; }); @@ -110,7 +111,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { { type: 'pFontSize', attrs: { fontSize: '8px' } }, { type: 'em' }, ], - attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false }, + attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false }, }, ], }, @@ -174,8 +175,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { showTitle = () => 'title'; setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { - const ay = DocListCast(a.links).length && DocCast(DocListCast(a.links)[0].anchor1).y; - const by = DocListCast(b.links).length && DocCast(DocListCast(b.links)[0].anchor1).y; + const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].anchor1).y; + const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].anchor1).y; return NumCast(ay) - NumCast(by); }; render() { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 7ba71eb5e..984b52ac8 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,12 +1,15 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Shadows } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { extname } from 'path'; -import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; -import { DashColor, emptyFunction, lightOrDark } from '../../Utils'; +import { Doc, Opt } from '../../fields/Doc'; +import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types'; +import { DashColor, lightOrDark } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocFocusOrOpen } from '../util/DocumentManager'; +import { LinkManager } from '../util/LinkManager'; +import { SelectionManager } from '../util/SelectionManager'; import { ColorScheme } from '../util/SettingsManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { TreeSort } from './collections/TreeView'; @@ -18,8 +21,6 @@ import { FieldViewProps } from './nodes/FieldView'; import { SliderBox } from './nodes/SliderBox'; import './StyleProvider.scss'; import React = require('react'); -import { Shadows } from 'browndash-components'; -import { SelectionManager } from '../util/SelectionManager'; export enum StyleProp { TreeViewIcon = 'treeViewIcon', @@ -88,7 +89,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const isAnnotated = property.includes(':annotated'); const isOpen = property.includes(':open'); const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : ''; - const isBackground = () => doc && BoolCast(doc._lockedPosition); + const lockedPosition = () => doc && BoolCast(doc._lockedPosition); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); @@ -111,16 +112,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; return allSorts; case StyleProp.Highlighting: - if (doc && !doc.noHighlighting) { - const highlightIndex = Doc.isBrushedHighlightedDegree(doc); - const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; + if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) { + const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc); + const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); + const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; - const excludeTypes = [DocumentType.FONTICON]; - let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') { + if (highlightIndex) { return { highlightStyle, - highlightColor: highlightIndex !== Doc.DocBrushStatus.highlighted && SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor, + highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK, }; @@ -217,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps docColor = docColor || 'transparent'; break; case DocumentType.LABEL: - docColor = docColor || (doc.annotationOn !== undefined ? 'rgba(128, 128, 128, 0.18)' : undefined) || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); + docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); @@ -261,14 +261,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps 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?.isLinkButton && DocListCast(doc?.links).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); + if (doc?.isLinkButton && 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 ? '4px 4px 10px 2px' - : isBackground() || doc?._isGroup || docProps?.LayoutTemplateString + : 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')}` ); @@ -279,27 +279,28 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return doc.z ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow : props?.ContainingCollectionDoc?._useClusters - ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 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.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 ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent - : isBackground() + ? `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 + : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big : StrCast(doc.boxShadow, ''); } } case StyleProp.PointerEvents: - if (MainView.Instance._exploreMode) return 'all'; + const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name); + if (MainView.Instance._exploreMode) return isInk ? 'visiblePainted' : 'all'; if (doc?.pointerEvents) return StrCast(doc.pointerEvents); + if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; - const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name); - if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none'; - if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active - return undefined; + if (opacity() === 0 || doc?.isInkMask) return 'none'; + if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all'; + return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) { - return doc && doc.pointerEvents === 'none' && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? ( + return doc && doc.pointerEvents === 'none' && lockedPosition() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> - <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" /> + <FontAwesomeIcon icon={lockedPosition() ? 'lock' : 'unlock'} style={{ color: lockedPosition() ? 'red' : undefined }} size="lg" /> </div> // <div/> ) : null; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 863829a51..681ff66e0 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,7 +1,6 @@ import { action, computed, observable, ObservableSet, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../fields/Doc'; -import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1ead80bd0..057c1e30f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -26,6 +26,9 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { ScriptField } from '../../../fields/ScriptField'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -407,11 +410,10 @@ export class CollectionDockingView extends CollectionSubView() { const _width = Number(getComputedStyle(content).width.replace('px', '')); const _height = Number(getComputedStyle(content).height.replace('px', '')); return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => { - const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight }); const proto = this.dataDoc; // Cast(img.proto, Doc, null)!; proto['thumb-nativeWidth'] = _width; proto['thumb-nativeHeight'] = _height; - this.dataDoc.thumb = new ImageField(iconFile); + proto.thumb = new ImageField(iconFile); }); } } @@ -580,15 +582,21 @@ ScriptingGlobals.add( '(doc: any)' ); ScriptingGlobals.add( - function openOnRight(doc: any) { - return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); + function openDoc(doc: any, where: OpenWhere) { + switch (where) { + 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); + } }, - 'opens up document in tab on right side of the screen', + 'opens up document in location specified', '(doc: any)' ); ScriptingGlobals.add( - function openInOverlay(doc: any) { - return Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + function openRepl() { + return 'openRepl'; }, 'opens up document in screen overlay layer', '(doc: any)' diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 6035871cd..fbf7db892 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -54,7 +54,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) { setTimeout(() => { - const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null)); + const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null) ?? []); const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); if (needsUnsetCategory || columnHeaders.length === 0) { columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 5a107d2ca..a55b70e22 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -97,7 +97,7 @@ top: 2.5%; height: 95%; border-radius: 4px; - background: $light-gray; + //background: $light-gray; &:hover { opacity: 1; } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 572fbfec2..4941bc722 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,19 +2,20 @@ import React = require('react'); import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; +import { emptyFunction, formatTime, OmitKeys, returnFalse, returnNone, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } 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 { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -179,12 +180,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack if ( // need to include range inputs because after dragging video time slider it becomes target element !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) && - this.props.isSelected(true) + this.props.isContentActive() ) { // if shift pressed scrub 1 second otherwise 1/10th const jump = e.shiftKey ? 1 : 0.1; switch (e.key) { case ' ': + this.props.playing() ? this.props.Pause() : this.props.Play(); + break; + case '^': if (!CollectionStackedTimeline.SelectingRegion) { this._markerStart = this._markerEnd = this.currentTime; CollectionStackedTimeline.SelectingRegion = this; @@ -398,8 +402,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any, _minFontSize: 12, _maxFontSize: 24, - _singleLine: false, + _singleLine: true, _stayInCollection: true, + backgroundColor: 'rgba(128, 128, 128, 0.5)', useLinkSmallAnchor: true, hideLinkButton: true, _isLinkButton: true, @@ -421,7 +426,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @action playOnClick = (anchorDoc: Doc, clientX: number) => { - const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlayAnchors) { if (this.props.playing()) this.props.Pause(); @@ -447,9 +452,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @action clickAnchor = (anchorDoc: Doc, clientX: number) => { if (anchorDoc.isLinkButton) { - LinkFollower.FollowLink(undefined, anchorDoc, this.props, false); + LinkFollower.FollowLink(undefined, anchorDoc, false); } - const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this.props.playing()) this.props.Pause(); @@ -563,7 +568,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack anchor, })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - return ( + return this.clipDuration === 0 ? null : ( <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> <div className="collectionStackedTimeline-timelineContainer" @@ -596,10 +601,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack top, width: `${width}px`, height: `${height}px`, - }} - onClick={e => { - this.props.playFrom(start, this.anchorEnd(d.anchor)); - e.stopPropagation(); + pointerEvents: 'none', }}> <StackedTimelineAnchor {...this.props} @@ -740,19 +742,19 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> () => this.props.currentTimecode(), time => { const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null); - const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); + const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); if ( !LightboxView.LightboxDoc && // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/ !this.props.layoutDoc.dontAutoFollowLinks && - DocListCast(this.props.mark.links).length && + LinkManager.Links(this.props.mark).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5 ) { - LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false); + LinkFollower.FollowLink(undefined, this.props.mark, false); } this._lastTimecode = time; } @@ -763,7 +765,9 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this._disposer?.(); } + @observable noEvents = false; // starting the drag event for anchor resizing + @action onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { //this.props._timeline?.setPointerCapture(e.pointerId); const newTime = (e: PointerEvent) => { @@ -781,8 +785,8 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> } return false; }; + this.noEvents = true; var undo: UndoManager.Batch | undefined; - setupMoveUpEvents( this, e, @@ -791,11 +795,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this.props.setTime(newTime(e)); return changeAnchor(anchor, left, newTime(e)); }, - e => { + action(e => { this.props.setTime(newTime(e)); - // this.props._timeline?.releasePointerCapture(e.pointerId); undo?.end(); - }, + this.noEvents = false; + }), emptyFunction ); }; @@ -813,10 +817,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> // renders anchor LabelBox renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) { const anchor = observable({ view: undefined as any }); - const focusFunc = (doc: Doc, options: DocFocusOptions) => { - this.props.playLink(mark); - this.props.focus(doc, options); - }; + const focusFunc = (doc: Doc, options: DocFocusOptions) => this.props.playLink(mark); return { anchor, view: ( @@ -826,6 +827,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> ref={action((r: DocumentView | null) => (anchor.view = r))} Document={mark} DataDoc={undefined} + pointerEvents={this.noEvents ? returnNone : undefined} styleProvider={this.props.styleProvider} renderDepth={this.props.renderDepth + 1} LayoutTemplate={undefined} @@ -856,15 +858,15 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> render() { const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.anchorScreenToLocalXf, this.width, this.height); return ( - <> + <div style={{ pointerEvents: this.noEvents ? 'none' : undefined }}> {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? null : ( <> - <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> - <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> + <div key="left" className="collectionStackedTimeline-left-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> + <div key="right" className="collectionStackedTimeline-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> </> )} - </> + </div> ); } } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index c88ae314e..bd74c9399 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,5 +1,4 @@ import { action, computed, observable } from 'mobx'; -import ReactLoading from 'react-loading'; import * as rp from 'request-promise'; import CursorField from '../../../fields/CursorField'; import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; @@ -270,10 +269,10 @@ export function CollectionSubView<X>(moreProps?: X) { if (FormattedTextBox.IsFragment(html)) { const href = FormattedTextBox.GetHref(html); if (href) { - const docid = FormattedTextBox.GetDocFromUrl(href); - if (docid) { + const docId = FormattedTextBox.GetDocFromUrl(href); + if (docId) { // prosemirror text containing link to dash document - DocServer.GetRefField(docid).then(f => { + DocServer.GetRefField(docId).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x as number; @@ -312,8 +311,8 @@ export function CollectionSubView<X>(moreProps?: X) { } else { const path = window.location.origin + '/doc/'; if (text.startsWith(path)) { - const docid = text.replace(Doc.globalServerPath(), '').split('?')[0]; - DocServer.GetRefField(docid).then(f => { + const docId = text.replace(Doc.globalServerPath(), '').split('?')[0]; + DocServer.GetRefField(docId).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x as number; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 330e116d1..553967b95 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -38,8 +38,8 @@ export type collectionTreeViewProps = { onCheckedClick?: () => ScriptField; onChildClick?: () => ScriptField; // TODO: [AL] add these fields - AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; - RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; + AddToMap?: (treeViewDoc: Doc, index: number[]) => void; + RemFromMap?: (treeViewDoc: Doc, index: number[]) => void; hierarchyIndex?: number[]; }; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 316f1e4e9..eafa50d27 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; import { ObjectField } from '../../../fields/ObjectField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -10,13 +9,12 @@ import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; -import { BranchCreate, BranchTask } from '../../documents/Gitlike'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; -import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OpenWhere } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; @@ -64,8 +62,8 @@ interface CollectionViewProps_ extends FieldViewProps { childClickScript?: ScriptField; childDoubleClickScript?: ScriptField; //TODO: [AL] add these fields - AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; - RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; + AddToMap?: (treeViewDoc: Doc, index: number[]) => void; + RemFromMap?: (treeViewDoc: Doc, index: number[]) => void; hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views) } export interface CollectionViewProps extends React.PropsWithChildren<CollectionViewProps_> {} @@ -169,7 +167,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab onContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== Doc.MainDocId) { + if (cm && !e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 this.setupViewTypes( 'UI Controls...', @@ -193,23 +191,23 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' }); - if (!Doc.noviceMode && false) { - optionItems.push({ - description: 'Create Branch', - event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), - icon: 'project-diagram', - }); - optionItems.push({ - description: 'Pull Master', - event: () => BranchTask(this.rootDoc, 'pull'), - icon: 'project-diagram', - }); - optionItems.push({ - description: 'Merge Branches', - event: () => BranchTask(this.rootDoc, 'merge'), - icon: 'project-diagram', - }); - } + // if (!Doc.noviceMode && false) { + // optionItems.push({ + // description: 'Create Branch', + // event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), + // icon: 'project-diagram', + // }); + // optionItems.push({ + // description: 'Pull Master', + // event: () => BranchTask(this.rootDoc, 'pull'), + // icon: 'project-diagram', + // }); + // optionItems.push({ + // description: 'Merge Branches', + // event: () => BranchTask(this.rootDoc, 'merge'), + // icon: 'project-diagram', + // }); + // } !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' }); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 99283996e..0ab94e2e3 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -57,7 +57,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { } @computed get tabBorderColor() { const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting); - if (highlight?.highlightIndex >= Doc.DocBrushStatus.highlighted) return highlight.highlightColor; + if (highlight?.highlightIndex === Doc.DocBrushStatus.highlighted) return highlight.highlightColor; return 'transparent'; } @computed get tabColor() { @@ -263,9 +263,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?.selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); if (!pinProps?.audioRange && duration !== undefined) { @@ -274,10 +271,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presStartTime = NumCast(doc.clipStart); pinDoc.presEndTime = NumCast(doc.clipEnd, duration); } - //PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, anchorDoc && anchorDoc !== doc && !anchorDoc.unrendered ? anchorDoc : doc); - pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)'); - Doc.AddDocToList(curPres, 'data', pinDoc, presSelected); - //save position if (pinProps?.activeFrame !== undefined) { pinDoc.presActiveFrame = pinProps?.activeFrame; pinDoc.title = doc.title + ' (move)'; @@ -294,6 +287,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presMovement = PresMovement.None; } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement()); PresBox.Instance?.clearSelectedArray(); pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array }); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 0d2968ba1..af2d148e0 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1,20 +1,21 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +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 { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; +import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -32,7 +33,6 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView'; import { CollectionView } from './CollectionView'; import './TreeView.scss'; import React = require('react'); -import { render } from 'react-dom'; export interface TreeViewProps { treeView: CollectionTreeView; @@ -66,8 +66,8 @@ export interface TreeViewProps { skipFields?: string[]; firstLevel: boolean; // TODO: [AL] add these - AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; - RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[]; + AddToMap?: (treeViewDoc: Doc, index: number[]) => void; + RemFromMap?: (treeViewDoc: Doc, index: number[]) => void; hierarchyIndex?: number[]; } @@ -169,37 +169,22 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get selected() { return SelectionManager.IsSelected(this._docRef); } - // SelectionManager.Views().lastElement()?.props.Document === this.props.document; } - - @observable _presTimer!: NodeJS.Timeout; - @observable _presKeyEventsActive: boolean = false; - - @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>(); - // the selected item's index - @computed get itemIndex() { - return NumCast(this.doc._itemIndex); - } - // the item that's active - @computed get activeItem() { - return this.childDocs ? Cast(this.childDocs[NumCast(this.doc._itemIndex)], Doc, null) : undefined; - } - @computed get targetDoc() { - return Cast(this.activeItem?.presentationTargetDoc, Doc, null); - } childDocList(field: string) { const layout = Cast(Doc.LayoutField(this.doc), Doc, null); - return ( - (this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCastOrNull(this.doc[field]) - ); // otherwise use the document's data field + return DocListCast(this.props.dataDoc?.[field], DocListCast(layout?.[field], DocListCast(this.doc[field]))); } + moving: boolean = false; @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { if (this.doc !== target && addDoc !== returnFalse) { + const canAdd1 = (this.props.parentTreeView as any).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this.props.parentTreeView?.doc.data)) instanceof ComputedField); + // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse - if (this.props.removeDoc?.(doc) === true) { - return addDoc(doc); + if (canAdd1 && this.props.removeDoc?.(doc) === true) { + this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = true); + const res = addDoc(doc); + this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = false); + return res; } } return false; @@ -207,6 +192,7 @@ 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 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.CollectionView)?.select(false); return res; @@ -378,20 +364,32 @@ export class TreeView extends React.Component<TreeViewProps> { 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.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { + if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.removeDocument, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { e.stopPropagation(); } } }; - dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) { + dropping: boolean = false; + dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, removeDocument: DragManager.RemoveFunction | undefined, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) { const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes('add')) || forceAdd; - const localAdd = (doc: Doc) => (Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false); - const addDoc = !inside ? parentAddDoc : (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean); + const localAdd = (doc: Doc | Doc[]) => { + 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.context = this.doc.context); + 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.containerCollection)?.freezeChildren).includes('add')) || forceAdd; if (canAdd) { - return UndoManager.RunInTempBatch(() => 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 = true); + const res = UndoManager.RunInTempBatch(() => 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; } return false; } @@ -437,12 +435,16 @@ export class TreeView extends React.Component<TreeViewProps> { const expandedWidth = () => this.props.panelWidth() - leftOffset.width; if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) { const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); - const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { - const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - added && (doc.context = this.doc.context); - return added; + const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc); + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => { + const innerAdd = (doc: Doc) => { + const 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.context = this.doc.context); + return added; + }; + return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); }; - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); contentElement = TreeView.GetChildElements( contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeView, @@ -453,7 +455,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.props.prevSibling, addDoc, remDoc, - this.move, + moveDoc, this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, @@ -533,6 +535,7 @@ export class TreeView extends React.Component<TreeViewProps> { ); const key = (expandKey === 'annotations' ? `${this.fieldKey}-` : '') + expandKey; const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); + const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc); const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't), // then the modification would be done here @@ -543,8 +546,10 @@ export class TreeView extends React.Component<TreeViewProps> { docs.push(doc); docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => (d.zIndex = i)); } - const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - added && (doc.context = this.doc.context); + 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.context = this.doc.context); + return added; }; const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); @@ -593,7 +598,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.props.prevSibling, addDoc, remDoc, - this.move, + moveDoc, StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, @@ -700,7 +705,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get validExpandViewTypes() { const annos = () => (DocListCast(this.doc[this.fieldKey + '-annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : ''); - const links = () => (DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? 'links' : ''); + const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : ''); const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : ''); const aliases = () => (this.props.treeView.dashboardMode ? '' : 'aliases'); const fields = () => (Doc.noviceMode ? '' : 'fields'); @@ -750,7 +755,7 @@ export class TreeView extends React.Component<TreeViewProps> { 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 openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: 'copy', label: 'Open Alias' }; + const openAlias = { script: ScriptField.MakeFunction(`openDoc(getAlias(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Alias' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' }; return [ ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), @@ -1028,7 +1033,9 @@ 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 ( + return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? ( + <div></div> + ) : ( <> {this.renderBullet} {this.renderTitle} @@ -1062,7 +1069,7 @@ export class TreeView extends React.Component<TreeViewProps> { const before = pt[1] < rect.top + rect.height / 2; const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); - const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, false)); + const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false)); }; render() { @@ -1146,8 +1153,8 @@ export class TreeView extends React.Component<TreeViewProps> { unobserveHeight: (ref: any) => void, contextMenuItems: { script: ScriptField; filter: ScriptField; label: string; icon: string }[], // TODO: [AL] add these - AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[], - RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[], + AddToMap?: (treeViewDoc: Doc, index: number[]) => void, + RemFromMap?: (treeViewDoc: Doc, index: number[]) => void, hierarchyIndex?: number[], renderCount?: number ) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 63b566f28..0ea614472 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -74,7 +74,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable public static ShowPresPaths = false; - private _lastNudge: any; + private _panZoomTransitionTimer: any; private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; @@ -94,9 +94,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _brushtimer: any; private _brushtimer1: any; - // private isWritingMode: boolean = true; - // private writingModeDocs: Doc[] = []; - private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } @@ -111,7 +108,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 @observable _hLines: number[] | undefined; @observable _vLines: number[] | undefined; - @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. + @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement @observable _showAnimTimeline = false; @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @@ -139,7 +136,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } @computed get fitContentsToBox() { - return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay; + return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; } @computed get contentBounds() { const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); @@ -181,8 +178,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } - _keyTimer: NodeJS.Timeout | undefined; - public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { if (timer) clearTimeout(timer); return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true); @@ -208,6 +203,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return newTimer; } + _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. changeKeyFrame = (back = false) => { const currentFrame = Cast(this.Document._currentFrame, 'number', null); if (currentFrame === undefined) { @@ -297,6 +293,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime); } + groupFocus = (anchor: Doc, options: DocFocusOptions) => { + options.docTransform = new Transform(-NumCast(this.rootDoc.panX) + NumCast(anchor.x), -NumCast(this.rootDoc.panY) + NumCast(anchor.y), 1); + const res = this.props.focus(this.rootDoc, options); + options.docTransform = undefined; + return res; + }; + focus = (anchor: Doc, options: DocFocusOptions) => { const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; @@ -305,6 +308,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // focus on the document in the collection const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); + if (didMove) options.didMove = true; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { const focusTime = options?.instant ? 0 : options.zoomTime ?? 500; @@ -520,7 +524,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._clusterSets.push([doc]); } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); - this._clusterSets[doc.cluster].push(doc); + this._clusterSets[doc.cluster ?? 0].push(doc); } } } @@ -583,14 +587,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) ) { + // prettier-ignore switch (Doc.ActiveTool) { - case InkTool.Highlighter: - break; - // TODO: nda - this where we want to create the new "writingDoc" collection that we add strokes to - case InkTool.Write: - break; - case InkTool.Pen: - break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views + case InkTool.Highlighter: break; + case InkTool.Write: break; + case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views case InkTool.Eraser: this._batch = UndoManager.StartBatch('collectionErase'); setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); @@ -1142,7 +1143,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) { - this._panZoomTransition = panTime; + this.setPanZoomTransition(panTime); const scale = this.getLocalTransform().inverse().Scale; const minScale = NumCast(this.rootDoc._viewScaleMin, 1); const minPanX = NumCast(this.rootDoc._panXMin, 0); @@ -1176,12 +1177,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action nudge = (x: number, y: number, nudgeTime: number = 500) => { if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) { - // bcz: this isn't ideal, but want to try it out... - this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true); - this._lastNudge && clearTimeout(this._lastNudge); - this._lastNudge = setTimeout( - action(() => (this._panZoomTransition = 0)), - nudgeTime + this.setPan( + NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale + NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), + nudgeTime, + true ); return true; } @@ -1207,9 +1207,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action + setPanZoomTransition = (transitionTime: number) => { + this._panZoomTransition = transitionTime; + this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer); + this._panZoomTransitionTimer = setTimeout( + action(() => (this._panZoomTransition = 0)), + transitionTime + ); + }; + + @action zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) { if (this.Document._isGroup) return; - this._panZoomTransition = transitionTime; + this.setPanZoomTransition(transitionTime); const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); @@ -1217,7 +1227,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0]; this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; - return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset } calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { @@ -1319,7 +1328,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection searchFilterDocs={this.searchFilterDocs} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={emptyFunction} - focus={this.isAnnotationOverlay ? this.props.focus : this.focus} + focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} addDocTab={this.addDocTab} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} @@ -1553,7 +1562,7 @@ 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: true, viewType: true, filters: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc); if (addAsAnnotation) { if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) { @@ -1574,7 +1583,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._firstRender = false; this._disposers.groupBounds = reaction( () => { - if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) { + 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]() })); return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } @@ -1795,11 +1804,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const mores = ContextMenu.Instance.findByDescription('More...'); const moreItems = mores && 'subitems' in mores ? mores.subitems : []; - if (!Doc.noviceMode) { - e.persist(); - moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) }); - } + moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) }); !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' }); }; @@ -2249,8 +2254,9 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY zoomTime: browseTransitionTime, }) === undefined ) { - const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + 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.props.scaleField || '_viewScale'] !== 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); @@ -2274,3 +2280,9 @@ ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: bool TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }) ); }); +ScriptingGlobals.add(function bringToFront() { + SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc)); +}); +ScriptingGlobals.add(function sendToBack(doc: Doc) { + SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true)); +}); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index a73627165..0714bffbc 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -396,7 +396,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection.forceActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; - newCollection.fitwidth = true; + newCollection.fitWidth = true; selected.forEach(d => (d.context = newCollection)); this.hideMarquee(); return newCollection; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index c9112eec3..3f6369898 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,7 +1,6 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; -import { DocCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index cdac91a62..29e7cd3ad 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; @@ -15,11 +15,11 @@ import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { undoBatch } from '../../util/UndoManager'; +import { MainView } from '../MainView'; import { DocumentView, OpenWhere } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; import React = require('react'); -import { MainView } from '../MainView'; interface LinkMenuItemProps { groupType: string; @@ -34,7 +34,7 @@ interface LinkMenuItemProps { // drag links and drop link targets (aliasing them if needed) export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { - const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; + const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; if (draggedDocs.length) { const moddrag: Doc[] = []; @@ -140,7 +140,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true }); - LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); + LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, false); } } ); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 4c26468b9..610c10be6 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -11,6 +11,7 @@ import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } f import { DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; +import { LinkManager } from '../../util/LinkManager'; import { undoBatch } from '../../util/UndoManager'; import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline'; import { ContextMenu } from '../ContextMenu'; @@ -84,7 +85,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return this.props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk @computed get links() { - return DocListCast(this.dataDoc.links); + return LinkManager.Links(this.dataDoc); } @computed get mediaState() { return this.dataDoc.mediaState as media_state; @@ -444,7 +445,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight); + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time); @@ -633,15 +634,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp step="0.1" min="1" max="5" - value={this.timeline?._zoomFactor} + value={this.timeline?._zoomFactor ?? 1} className="toolbar-slider" id="zoom-slider" - onPointerDown={(e: React.PointerEvent) => { - e.stopPropagation(); - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - this.zoom(Number(e.target.value)); - }} + onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))} /> </div> )} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 901415d1e..9bdb2cee7 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -7,7 +7,6 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { numberRange } from '../../../Utils'; -import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 569579996..679c10a20 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,4 +1,4 @@ -import { computed, trace } from 'mobx'; +import { computed } from 'mobx'; import { observer } from 'mobx-react'; import { AclPrivate, Doc, Opt } from '../../../fields/Doc'; import { ScriptField } from '../../../fields/ScriptField'; @@ -24,7 +24,6 @@ import { DocumentViewProps } from './DocumentView'; import './DocumentView.scss'; import { EquationBox } from './EquationBox'; import { FieldView, FieldViewProps } from './FieldView'; -import { FilterBox } from './FilterBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; @@ -32,6 +31,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LabelBox } from './LabelBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkBox } from './LinkBox'; +import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; import { PDFBox } from './PDFBox'; import { RecordingBox } from './RecordingBox'; @@ -43,7 +43,6 @@ import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import React = require('react'); import XRegExp = require('xregexp'); -import { LoadingBox } from './LoadingBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -254,7 +253,6 @@ export class DocumentContentsView extends React.Component< YoutubeBox, PresElementBox, SearchBox, - FilterBox, FunctionPlotBox, ColorBox, DashWebRTCVideo, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index dcccd1143..c1665ea3d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -49,7 +49,6 @@ import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { LinkAnchorBox } from './LinkAnchorBox'; -import { LinkDocPreview } from './LinkDocPreview'; import { RadialMenu } from './RadialMenu'; import { ScriptingBox } from './ScriptingBox'; import { PresEffect, PresEffectDirection } from './trails'; @@ -81,6 +80,7 @@ export enum OpenWhere { replaceLeft = 'replace:left', inParent = 'inParent', inParentFromScreen = 'inParentFromScreen', + overlay = 'overlay', } export enum OpenWhereMod { none = '', @@ -95,6 +95,7 @@ export interface DocFocusOptions { willZoomCentered?: boolean; // determines whether to zoom in on target document zoomScale?: number; // percent of containing frame to zoom into document zoomTime?: number; + didMove?: boolean; // whether a document was changed during the showDocument process docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) preview?: boolean; // whether changes should be previewed by the componentView or written to the document @@ -104,7 +105,7 @@ export interface DocFocusOptions { openLocation?: string; // where to open a missing document zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections toggleTarget?: boolean; // whether to toggle target on and off - originatingDoc?: Doc; // document that triggered the focus + anchorDoc?: Doc; // doc containing anchor info to apply at end of focus to target doc easeFunc?: 'linear' | 'ease'; // transition method for scrolling } export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>; @@ -161,6 +162,7 @@ export interface DocumentViewSharedProps { childHideResizeHandles?: () => boolean; 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; fitWidth?: (doc: Doc) => boolean | undefined; docFilters: () => string[]; @@ -313,7 +315,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - @computed get pointerEvents() { + @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); } @computed get finalLayoutKey() { @@ -538,18 +540,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .ScreenToLocalTransform() .scale(this.NativeDimScaling) .transformDirection(x - left, y - top); - // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document - // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); dragData.dropAction = dropAction; dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; dragData.canEmbed = this.props.canEmbedOnDrag; - //dragData.dimSource : - // dragEffects field, set dim - // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping - // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging: - // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () => @@ -559,22 +554,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } - onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey) { - e.stopPropagation(); - e.preventDefault(); - if (e.key === '†' || e.key === 't') { - if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; - if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true)); - else if (!this._titleRef.current.setIsFocused(true)) { - // if focus didn't change, focus on interior text... - this._titleRef.current?.setIsFocused(false); - this._componentView?.setFocus?.(); - } - } - } - }; - 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 @@ -584,6 +563,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; }; + // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been) + setTitleFocus = () => { + if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; + setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined + }; + onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { let stopPropagate = true; @@ -613,6 +598,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps altKey, shiftKey, ctrlKey, + value: undefined, }, console.log ); @@ -654,7 +640,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } else clickFunc(); } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { SelectionManager.DeselectAll(); - this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey); + this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // 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 @@ -930,7 +916,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, icon: 'eye', }); - DocListCast(this.Document.links).length && + LinkManager.Links(this.Document).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' }); !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); @@ -970,7 +956,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(false, true), icon: 'map-pin' }); 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...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); - } else if (DocListCast(this.Document.links).length) { + } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' }); onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' }); @@ -989,7 +975,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const more = cm.findByDescription('More...'); const moreItems = more && 'subitems' in more ? more.subitems : []; if (!Doc.IsSystem(this.rootDoc)) { - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); if (!Doc.noviceMode) { moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' }); moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' }); @@ -1001,6 +986,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); } + moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); + + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); } if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) { @@ -1128,22 +1116,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </Tooltip> ); } + pointerEventsFunc = () => this.pointerEvents; @computed get contents() { TraceMobx(); return ( <div className="documentView-contentsView" style={{ - pointerEvents: - this.opacity === 0 - ? 'none' - : (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon' - ? 'none' - : (this.props.contentPointerEvents as any) - ? (this.props.contentPointerEvents as any) - : this.rootDoc.type !== DocumentType.INK && this.isContentActive() - ? 'all' - : 'none', + // prettier-ignore + pointerEvents: + (this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents) ?? + (this.rootDoc.layoutKey === 'layout_icon' ? 'none' : 'all'), height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? null : ( @@ -1164,7 +1147,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <DocumentContentsView key={1} {...this.props} - pointerEvents={this.opacity === 0 ? returnNone : this.props.pointerEvents} + pointerEvents={this.opacity === 0 ? returnNone : this.pointerEventsFunc} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} isHovering={this.props.isHovering} @@ -1176,7 +1159,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ScreenToLocalTransform={this.screenToLocal} rootSelected={this.rootSelected} onClick={this.onClickFunc} - focus={emptyFunction} + focus={this.props.focus} + setTitleFocus={this.setTitleFocus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} @@ -1493,7 +1477,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont} onContextMenu={this.onContextMenu} - onKeyDown={this.onKeyDown} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} @@ -1502,7 +1485,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ display: this.hidden ? 'inline' : undefined, borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents, + pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, }}> {!borderPath.path ? ( animRenderDoc @@ -1541,11 +1524,17 @@ export class DocumentView extends React.Component<DocumentViewProps> { } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); public ViewTimer: NodeJS.Timeout | undefined; // timer for res + public AnimEffectTimer: NodeJS.Timeout | undefined; // timer for res private _disposers: { [name: string]: IReactionDisposer } = {}; public clearViewTransition = () => { this.ViewTimer && clearTimeout(this.ViewTimer); this.rootDoc._viewTransition = undefined; }; + 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); + }; public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`; @@ -1573,8 +1562,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { } public static showBackLinks(linkSource: Doc) { - const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; - DocServer.GetRefField(docid).then(docx => { + const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; + DocServer.GetRefField(docId).then(docx => { const rootAlias = () => { const rootAlias = Doc.MakeAlias(linkSource); rootAlias.x = rootAlias.y = 0; @@ -1587,7 +1576,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { /*rootAlias()*/ ], { title: linkSource.title + '-pivot', _width: 500, _height: 500 }, - docid + docId ); linkCollection.linkSource = linkSource; if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)'); @@ -1902,7 +1891,7 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { const linkSource = Cast(linkCollection.linkSource, Doc, null); const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); let wid = linkSource[WidthSym](); - const links = DocListCast(linkSource.links); + 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; diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss index 107ad2e36..7f907c8d4 100644 --- a/src/client/views/nodes/FilterBox.scss +++ b/src/client/views/nodes/FilterBox.scss @@ -16,7 +16,6 @@ } } - .filter-bookmark { //display: flex; @@ -39,7 +38,6 @@ // margin-bottom: 15px; } - .filterBox-saveBookmark { background-color: #e9e9e9; border-radius: 11px; @@ -61,7 +59,6 @@ margin-top: 4px; margin-left: 2px; } - } .filterBox-select-scope, @@ -154,12 +151,11 @@ display: flex; flex-direction: column; width: 200px; - height: 100%; position: absolute; right: 0; top: 0; z-index: 1; - background-color: #9F9F9F; + background-color: #9f9f9f; .filterBox-tree { z-index: 0; @@ -177,8 +173,8 @@ cursor: pointer; } - >div, - >div>div { + > div, + > div > div { width: 100%; height: 100%; } @@ -190,4 +186,4 @@ margin-bottom: 10px; //height: calc(100% - 30px); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index d41a4bb82..e69de29bb 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -1,588 +0,0 @@ -import React = require('react'); -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import Select from 'react-select'; -import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt } from '../../../fields/Doc'; -import { List } from '../../../fields/List'; -import { RichTextField } from '../../../fields/RichTextField'; -import { listSpec } from '../../../fields/Schema'; -import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; -import { Docs, DocumentOptions } from '../../documents/Documents'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { UserOptions } from '../../util/GroupManager'; -import { ScriptingGlobals } from '../../util/ScriptingGlobals'; -import { SelectionManager } from '../../util/SelectionManager'; -import { CollectionTreeView } from '../collections/CollectionTreeView'; -import { CollectionView } from '../collections/CollectionView'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { EditableView } from '../EditableView'; -import { SearchBox } from '../search/SearchBox'; -import { DashboardToggleButton, DefaultStyleProvider, StyleProp } from '../StyleProvider'; -import { DocumentViewProps } from './DocumentView'; -import { FieldView, FieldViewProps } from './FieldView'; -import './FilterBox.scss'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - -@observer -export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { - constructor(props: Readonly<FieldViewProps>) { - super(props); - const targetDoc = FilterBox.targetDoc; - if (targetDoc && !targetDoc.currentFilter) runInAction(() => (targetDoc.currentFilter = FilterBox.createFilterDoc())); - } - - /// creates a new, empty filter doc - static createFilterDoc() { - const clearAll = `getProto(self).data = new List([])`; - const reqdOpts: DocumentOptions = { - _lockedPosition: true, - _autoHeight: true, - _fitWidth: true, - _height: 150, - _xPadding: 5, - _yPadding: 5, - _gridGap: 5, - _forceActive: true, - title: 'Unnamed Filter', - filterBoolean: 'AND', - boxShadow: '0 0', - childDontRegisterViews: true, - targetDropAction: 'same', - ignoreClick: true, - system: true, - childDropAction: 'none', - treeViewHideTitle: true, - treeViewTruncateTitleWidth: 150, - childContextMenuLabels: new List<string>(['Clear All']), - childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]), - }; - return Docs.Create.FilterDocument(reqdOpts); - } - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(FilterBox, fieldKey); - } - - public _filterSelected = false; - public _filterMatch = 'matched'; - - @computed static get currentContainingCollectionDoc() { - let docView: any = SelectionManager.Views()[0]; - let doc = docView.Document; - - while (docView && doc && doc.type !== 'collection') { - doc = docView.props.ContainingCollectionDoc; - docView = docView.props.ContainingCollectionView; - } - - return doc; - } - - // /** - // * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection - // */ - - // trying to get this to work rather than the version below this - - // @computed static get targetDoc() { - // console.log(FilterBox.currentContainingCollectionDoc.type); - // if (FilterBox._filterScope === "Current Collection") { - // return FilterBox.currentContainingCollectionDoc; - // } - // else return CollectionDockingView.Instance.props.Document; - // // return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; - // } - - /** - * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection - */ - @computed static get targetDoc() { - return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : Doc.ActiveDashboard; - } - @computed static get targetDocChildKey() { - if (SelectionManager.Views().length) { - return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || 'data'; - } - return 'data'; - } - @computed static get targetDocChildren() { - return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || Doc.ActiveDashboard?.data); - } - - @observable _loaded = false; - componentDidMount() { - reaction( - () => DocListCastAsync(this.layoutDoc[this.fieldKey]), - async activeTabsAsync => { - const activeTabs = await activeTabsAsync; - activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction)); - runInAction(() => (this._loaded = true)); - }, - { fireImmediately: true } - ); - } - - @computed get allDocs() { - // trace(); - const allDocs = new Set<Doc>(); - const targetDoc = FilterBox.targetDoc; - if (this._loaded && targetDoc) { - SearchBox.foreachRecursiveDoc(FilterBox.targetDocChildren, (depth, doc) => allDocs.add(doc)); - } - return Array.from(allDocs); - } - - @computed get _allFacets() { - // trace(); - const noviceReqFields = ['author', 'tags', 'text', 'type']; - const noviceLayoutFields: string[] = []; //["_curPage"]; - const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; - - const keys = new Set<string>(noviceFields); - this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); - return Array.from(keys.keys()) - .filter(key => key[0]) - .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) - .sort(); - } - - /** - * The current attributes selected to filter based on - */ - @computed get activeAttributes() { - return DocListCast(this.dataDoc[this.props.fieldKey]); - } - - /** - * @returns a string array of the current attributes - */ - @computed get currentFacets() { - return this.activeAttributes.map(attribute => StrCast(attribute.title)); - } - - gatherFieldValues(childDocs: Doc[], facetKey: string) { - const valueSet = new Set<string>(); - let rtFields = 0; - childDocs.forEach(d => { - const facetVal = d[facetKey]; - if (facetVal instanceof RichTextField) rtFields++; - valueSet.add(Field.toString(facetVal as Field)); - const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); - const data = d[annos ? fieldKey + '-annotations' : fieldKey]; - if (data !== undefined) { - let subDocs = DocListCast(data); - if (subDocs.length > 0) { - let newarray: Doc[] = []; - while (subDocs.length > 0) { - newarray = []; - subDocs.forEach(t => { - const facetVal = t[facetKey]; - if (facetVal instanceof RichTextField) rtFields++; - facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); - 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)); - }); - subDocs = newarray; - } - } - } - }); - return { strings: Array.from(valueSet.keys()), rtFields }; - } - removeFilterDoc = (doc: Doc | Doc[]) => ((doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false); - public removeFilter = (filterName: string) => { - const targetDoc = FilterBox.targetDoc; - if (targetDoc) { - const filterDoc = targetDoc.currentFilter as Doc; - const attributes = DocListCast(filterDoc.data); - const found = attributes.findIndex(doc => doc.title === filterName); - if (found !== -1) { - (filterDoc.data as List<Doc>).splice(found, 1); - const docFilter = Cast(targetDoc._docFilters, listSpec('string')); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item.split(':')[0] === filterName)) !== -1) { - docFilter.splice(index, 1); - } - } - const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec('string')); - if (docRangeFilters) { - let index: number; - while ((index = docRangeFilters.findIndex(item => item.split(':')[0] === filterName)) !== -1) { - docRangeFilters.splice(index, 3); - } - } - } - } - return true; - }; - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const { targetDoc, targetDocChildren } = FilterBox; - if (!targetDoc) return; - const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader); - if (found !== -1) { - this.removeFilter(facetHeader); - } else { - const allCollectionDocs = targetDocChildren; - const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader); - - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, - maxVal = -Number.MAX_VALUE; - facetValues.strings.map(val => { - const num = val ? Number(val) : Number.NaN; - if (Number.isNaN(num)) { - val && nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - let newFacet: Opt<Doc>; - if (facetHeader === 'text' || facetValues.rtFields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument('', { - title: facetHeader, - system: true, - target: targetDoc, - _width: 100, - _height: 25, - _stayInCollection: true, - treeViewExpandedView: 'layout', - _treeViewOpen: true, - _forceActive: true, - ignoreClick: true, - }); - Doc.GetProto(newFacet).forceActive = true; // required for FormattedTextBox to be able to gain focus since it will never be 'selected' - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4; - const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; - newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: 'string' }); - } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { - newFacet = Docs.Create.SliderDocument({ - title: facetHeader, - system: true, - target: targetDoc, - _fitWidth: true, - _height: 40, - _stayInCollection: true, - treeViewExpandedView: 'layout', - _treeViewOpen: true, - _forceActive: true, - _overflow: 'visible', - }); - const newFacetField = Doc.LayoutFieldKey(newFacet); - const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); - const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); - newFacet[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; - newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + '-minThumb'] = extendedMinVal; - Doc.GetProto(newFacet)[newFacetField + '-maxThumb'] = extendedMaxVal; - const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: 'number' }); - newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); - } else { - newFacet = new Doc(); - newFacet.system = true; - newFacet.title = facetHeader; - newFacet.treeViewOpen = true; - newFacet.layout = CollectionView.LayoutString('data'); - newFacet.layoutKey = 'layout'; - newFacet.type = DocumentType.COL; - newFacet.target = targetDoc; - newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); - } - newFacet.hideContextMenu = true; - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); - } - }; - - @computed get scriptField() { - const scriptText = 'setDocFilter(this?.target, heading, this.title, checked)'; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name }); - return script ? () => script : undefined; - } - - /** - * Sets whether filters are ANDed or ORed together - */ - @action - changeBool = (e: any) => { - FilterBox.targetDoc && (DocCast(FilterBox.targetDoc.currentFilter).filterBoolean = e.currentTarget.value); - }; - - /** - * Changes whether to select matched or unmatched documents - */ - @action - changeMatch = (e: any) => { - this._filterMatch = e.currentTarget.value; - }; - - @action - changeSelected = () => { - if (this._filterSelected) { - this._filterSelected = false; - SelectionManager.DeselectAll(); - } else { - this._filterSelected = true; - // helper method to select specified docs - } - }; - - FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { - switch (property.split(':')[0]) { - case StyleProp.Decorations: - if (doc && !doc.treeViewHideHeaderFields) { - return ( - <> - <div style={{ marginRight: '5px', fontSize: '10px' }}> - <select className="filterBox-selection"> - <option value="Is" key="Is"> - Is - </option> - <option value="Is Not" key="Is Not"> - Is Not - </option> - </select> - </div> - <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}> - <FontAwesomeIcon icon={'times'} size="sm" /> - </div> - </> - ); - } - } - return DefaultStyleProvider(doc, props, property); - } - - suppressChildClick = () => ScriptField.MakeScript('')!; - - /** - * Adds a filterDoc to the list of saved filters - */ - saveFilter = () => { - Doc.AddDocToList(Doc.UserDoc(), 'savedFilters', this.props.Document); - }; - - /** - * Changes the title of the filterDoc - */ - onTitleValueChange = (val: string) => { - Doc.GetProto(this.props.Document).title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`; - return true; - }; - - /** - * The flyout from which you can select a saved filter to apply - */ - @computed get flyoutPanel() { - return DocListCast(Doc.UserDoc().savedFilters).map(doc => { - return ( - <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 20, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}> - {StrCast(doc.title)} - </div> - ); - }); - } - setTreeHeight = (hgt: number) => { - this.layoutDoc._height = NumCast(this.layoutDoc._autoHeightMargins) + 150; // need to add all the border sizes together. - }; - - /** - * add lock and hide button decorations for the "Dashboards" flyout TreeView - */ - FilterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => { - if (property.split(':')[0] === StyleProp.Decorations) { - return !doc || doc.treeViewHideHeaderFields ? null : DashboardToggleButton(doc, 'hidden', 'trash', 'trash', () => this.removeFilter(StrCast(doc.title))); - } - return this.props.styleProvider?.(doc, props, property); - }; - - layoutHeight = () => this.layoutDoc[HeightSym](); - render() { - const facetCollection = this.props.Document; - - const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); - return this.props.dontRegisterView ? null : ( - <div className="filterBox-treeView" style={{ width: '100%' }}> - <div className="filterBox-title"> - <EditableView key="editableView" contents={this.props.Document.title} height={24} fontSize={15} GetValue={() => StrCast(this.props.Document.title)} SetValue={this.onTitleValueChange} /> - </div> - - <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc?.currentFilter as Doc)?.filterBoolean)}> - <option value="AND" key="AND"> - AND - </option> - <option value="OR" key="OR"> - OR - </option> - </select> - <div className="filterBox-select-text">filters together</div> - </div> - - <div className="filterBox-select"> - <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-tree" key="tree"> - <CollectionTreeView - Document={facetCollection} - DataDoc={Doc.GetProto(facetCollection)} - fieldKey={this.props.fieldKey} - CollectionView={undefined} - disableDocBrushing={true} - setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in. - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - childDocumentsActive={returnTrue} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.layoutHeight} - rootSelected={this.props.rootSelected} - renderDepth={1} - dropAction={this.props.dropAction} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - isAnyChildContentActive={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - isSelected={returnFalse} - select={returnFalse} - bringToFront={emptyFunction} - isContentActive={returnTrue} - whenChildContentsActiveChanged={returnFalse} - treeViewHideTitle={true} - focus={emptyFunction} - onCheckedClick={this.scriptField} - treeViewHideHeaderFields={false} - dontRegisterView={true} - styleProvider={this.FilterStyleProvider} - docViewPath={this.props.docViewPath} - scriptContext={this.props.scriptContext} - moveDocument={returnFalse} - removeDocument={this.removeFilterDoc} - addDocument={returnFalse} - /> - </div> - <div className="filterBox-bottom"> - {/* <div className="filterBox-select-matched"> - <input className="filterBox-select-box" type="checkbox" - onChange={this.changeSelected} /> - <div className="filterBox-select-text">select</div> - <select className="filterBox-selection" onChange={e => this.changeMatch(e)}> - <option value="matched" key="matched">matched</option> - <option value="unmatched" key="unmatched">unmatched</option> - </select> - <div className="filterBox-select-text">documents</div> - </div> */} - - <div style={{ display: 'flex' }}> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark" onPointerDown={this.saveFilter}> - <div>SAVE</div> - </div> - </div> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark"> - <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}> - <div>FILTERS</div> - </Flyout> - </div> - </div> - <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark" onPointerDown={this.props.createNewFilterDoc}> - <div>NEW</div> - </div> - </div> - </div> - </div> - </div> - ); - } -} - -ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { - const docFilters = Cast(layoutDoc._docFilters, listSpec('string'), []); - for (const filter of docFilters) { - const fields = filter.split(':'); // split into key:value:modifiers - if (fields[0] === facetHeader && fields[1] === facetValue) { - return fields[2]; - } - } - return undefined; -}); -ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, childKey: string, facetHeader: string) { - const allCollectionDocs = new Set<Doc>(); - const activeTabs = DocListCast(layoutDoc[childKey]); - SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); - const set = new Set<string>(); - if (facetHeader === 'tags') - allCollectionDocs.forEach(child => - Field.toString(child[facetHeader] as Field) - .split(':') - .forEach(key => set.add(key)) - ); - else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); - const facetValues = Array.from(set).filter(v => v); - - let minVal = Number.MAX_VALUE, - maxVal = -Number.MAX_VALUE; - facetValues.map(val => { - const num = val ? Number(val) : Number.NaN; - if (!Number.isNaN(num)) { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - const newFacetField = Doc.LayoutFieldKey(facetDoc); - const ranged = Doc.readDocRangeFilter(layoutDoc, facetHeader); - const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); - const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); - facetDoc[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; - facetDoc[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(facetDoc)[newFacetField + '-minThumb'] = extendedMinVal; - Doc.GetProto(facetDoc)[newFacetField + '-maxThumb'] = extendedMaxVal; -}); -ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) { - const allCollectionDocs = new Set<Doc>(); - const activeTabs = DocListCast(layoutDoc[childKey]); - SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); - const set = new Set<string>(); - if (facetHeader === 'tags') - allCollectionDocs.forEach(child => - Field.toString(child[facetHeader] as Field) - .split(':') - .forEach(key => set.add(key)) - ); - else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); - const facetValues = Array.from(set).filter(v => v); - - let nonNumbers = 0; - - facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); - const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { - const doc = new Doc(); - doc.system = true; - doc.title = facetValue.toString(); - doc.target = layoutDoc; - doc.facetHeader = facetHeader; - doc.facetValue = facetValue; - doc.treeViewHideHeaderFields = true; - doc.treeViewChecked = ComputedField.MakeFunction('determineCheckedState(self.target, self.facetHeader, self.facetValue)'); - return doc; - }); - return new List<Doc>(facetValueDocSet); -}); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c6d4cb694..e60a03190 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -27,7 +27,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; +import { DocFocusOptions, OpenWhere } from './DocumentView'; import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; @@ -73,7 +73,15 @@ 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({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + Docs.Create.ImageanchorDocument({ + title: 'ImgAnchor:' + this.rootDoc.title, + presPanX: NumCast(this.layoutDoc._panX), + presPanY: NumCast(this.layoutDoc._panY), + presViewScale: Cast(this.layoutDoc._viewScale, 'number', null), + presTransition: 1000, + unrendered: true, + annotationOn: this.rootDoc, + }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; /* addAsAnnotation &&*/ this.addDocument(anchor); @@ -433,6 +441,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._marqueeing = undefined; this.props.select(false); }; + focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options); + + _ffref = React.createRef<CollectionFreeFormView>(); savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); @@ -460,6 +471,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined, }}> <CollectionFreeFormView + ref={this._ffref} {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} @@ -471,6 +483,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp PanelHeight={this.props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} + focus={this.focus} getScrollHeight={this.getScrollHeight} NativeDimScaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 33fab6873..6e0b4be37 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -79,7 +79,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp @observable _mouseOver = false; @computed get hoverColor() { - return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset'; + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); } fitTextToBox = (r: any): any => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 1b37dc7ab..e12548f18 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -39,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { this.onPointerMove, emptyFunction, (e, doubleTap) => { - if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); + if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, false); else this.props.select(false); }, false diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index bbe9b4323..fcc5b6975 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; -import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; @@ -14,11 +14,10 @@ import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; +import { SearchBox } from '../search/SearchBox'; import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); -import { SearchUtil } from '../../util/SearchUtil'; -import { SearchBox } from '../search/SearchBox'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -110,8 +109,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined; anchorDoc?.then?.( action(anchor => { - if (anchor instanceof Doc && DocListCast(anchor.links).length) { - this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0]; + if (anchor instanceof Doc && LinkManager.Links(anchor).length) { + this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0]; const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords; if (automaticLink) { // automatic links specify the target in the link info, not the source @@ -172,7 +171,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { followLink = () => { LinkDocPreview.Clear(); if (this._linkDoc && this._linkSrc) { - LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); + LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this.props.hrefs?.length) { const webDoc = Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index c182dfd19..0064a2022 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -21,6 +21,7 @@ import { AnchorMenu } from '../../pdf/AnchorMenu'; import { Annotation } from '../../pdf/Annotation'; import { SidebarAnnos } from '../../SidebarAnnos'; import { FieldView, FieldViewProps } from '../FieldView'; +import { PinProps } from '../trails'; import './MapBox.scss'; import { MapBoxInfoWindow } from './MapBoxInfoWindow'; @@ -133,8 +134,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - constructor(props: any) { - super(props); + componentDidMount() { + this.props.setContentView?.(this); } @action @@ -525,7 +526,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } - getAnchor = (addAsAnnotation: boolean) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; /** * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index fa2021642..a6e2c3e90 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -180,13 +180,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const params: ScriptParam = {}; this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); - const result = CompileScript(this.rawText, { - editable: true, - transformer: DocumentIconContainer.getTransformer(), - params, - typecheck: false, - }); - Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true); + const result = !this.rawText.trim() + ? ({ compiled: false, errors: undefined } as any) + : CompileScript(this.rawText, { + editable: true, + transformer: DocumentIconContainer.getTransformer(), + params, + typecheck: false, + }); + Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true); this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 7df3d5f24..dc6bf6f9e 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,7 +3,7 @@ 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, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -14,6 +14,7 @@ import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; +import { LinkManager } from '../../util/LinkManager'; import { ReplayMovements } from '../../util/ReplayMovements'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -85,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _scrubbing: boolean = false; @computed get links() { - return DocListCast(this.dataDoc.links); + return LinkManager.Links(this.dataDoc); } @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index b1acef44a..f7425b26a 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -145,6 +145,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); if ( + !this.props.isSelected(true) && + !Doc.IsBrushedDegree(this.rootDoc) && + !this.isAnyChildContentActive() && !this.rootDoc.thumbLockout && !this.props.dontRegisterView && this._iframe && @@ -181,6 +184,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); } }; + _thumbTimer: any; async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) @@ -197,6 +201,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), async selected => { if (selected) { + this._thumbTimer && clearTimeout(this._thumbTimer); this._webPageHasBeenRendered = true; } else if ( (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) @@ -204,7 +209,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps LightboxView.LightboxDoc !== this.rootDoc ) { // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - this.updateThumb(); + this._thumbTimer && clearTimeout(this._thumbTimer); + this._thumbTimer = setTimeout(this.updateThumb, 2000); } }, { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts deleted file mode 100644 index b4a382faf..000000000 --- a/src/client/views/nodes/button/ButtonScripts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { Colors } from "../../global/globalEnums"; - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function changeView(view: string) { - const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed"); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) { - const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent"; - selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed"); -});
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss index a1ca777b3..7fe1436c7 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -43,10 +43,6 @@ cursor: pointer; flex-direction: column; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - svg { width: 50% !important; height: 50%; @@ -68,10 +64,6 @@ justify-content: center; align-items: center; justify-items: center; - - &:hover { - filter: brightness(0.85) !important; - } } &.tglBtn, @@ -220,10 +212,6 @@ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); border-radius: 3px; } - - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } } &.colorBtnLabel { @@ -248,10 +236,6 @@ align-content: center; align-items: center; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - .menuButton-dropdownList { position: absolute; width: 150px; @@ -283,10 +267,6 @@ cursor: pointer; background: transparent; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - &.slider { color: $white; cursor: pointer; diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 0468d1c47..cb962cad3 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -11,7 +11,7 @@ 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, StopEvent, Utils } from '../../../../Utils'; +import { aggregateBounds, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { LinkManager } from '../../../util/LinkManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -100,7 +100,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); } Icon = (color: string) => { - const icon = StrCast(this.dataDoc.icon, 'user') as any; + 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} />; }; @@ -134,10 +134,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get numberButton() { const numBtnType: string = StrCast(this.rootDoc.numBtnType); const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ value, _readOnly_: false }), 'set num value'); + 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({ value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); + 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>; @@ -150,7 +150,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult} - className={'menu-slider'} + className="menu-slider" onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))} onPointerUp={() => this._batch?.end()} onChange={e => { @@ -284,27 +284,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() { text = 'User Default'; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; - } else if (script?.script.originalScript.startsWith('setFont')) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text']; - } + } 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.includes(value)) + .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) .map(value => ( <div className="list-item" key={`${value}`} style={{ - fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined, + fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, }} - onClick={undoBatch(() => script.script.run({ value }))}> + onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}> {value[0].toUpperCase() + value.slice(1)} </div> )); @@ -357,7 +353,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { ev.preventDefault(); ev.stopPropagation(); const s = this.colorScript; - s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)(); + 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} />; @@ -368,7 +364,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @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({ value: undefined, _readOnly_: true }).result ?? 'transparent'; + const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent'; const label = !this.label || !FontIconBox.GetShowLabels() ? null : ( @@ -377,13 +373,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div> ); - // dropdown caret seems superfluous since clicking the color button does the same thing - // const dropdownCaret = <div - // className="menuButton-dropDown" - // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> - // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> - // </div>; - //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`} @@ -598,121 +587,70 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -/** TEXT - * setFont - * setFontSize - * toggleBold - * toggleUnderline - * toggleItalic - * setAlignment - * toggleBold - * toggleItalic - * toggleUnderline - **/ - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) { - if (checkResult) return RichTextMenu.Instance?.fontFamily; - font && RichTextMenu.Instance.setFontFamily(font); -}); - -ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') { - const editorView = RichTextMenu.Instance.TextView?.EditorView; - const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection(); - // prettier-ignore - switch (info) { - case 'family': return style?.activeFamilies[0]; - case 'size': return style?.activeSizes[0]; - case 'color': return style?.activeColors[0]; - case 'highlight': return style?.activeHighlights[0]; - } -}); - -ScriptingGlobals.add(function setAlignment(align: 'left' | 'right' | 'center', checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align); - else Doc.UserDoc().textAlign = align; -}); - -ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', checkResult?: boolean) { +ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); - if (active === mapStyle) return Colors.MEDIUM_BLUE; - return 'transparent'; - } - editorView?.state && RichTextMenu.Instance.changeListType(mapStyle); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) { - if (checkResult) return RichTextMenu.Instance.fontColor; - color && RichTextMenu.Instance.setColor(color); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); - const editorView = RichTextMenu.Instance.TextView?.EditorView; + // prettier-ignore + const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void; setMode?: () => void }> = new Map([ + ['font', { + checkResult: () => RichTextMenu.Instance?.fontFamily, + setDoc: () => value && RichTextMenu.Instance.setFontFamily(value), + setMode: () => Doc.UserDoc().textAlign = 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 (selected ?? Doc.UserDoc())._fontHighlight; + return map.get(attr)?.checkResult(); } - color && RichTextMenu.Instance.setHighlight(color); + if (editorView?.state) map.get(attr)?.setDoc(); + else map.get(attr)?.setMode?.(); }); -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) { - if (checkResult) { - return RichTextMenu.Instance?.fontSize.replace('px', ''); - } - if (typeof size === 'number') size = size.toString(); - if (size && Number(size).toString() === size) size += 'px'; - RichTextMenu.Instance.setFontSize(size); -}); -ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); -}); -ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) { +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; - if (checkResult) { - return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (textView) runInAction(() => (textView._recording = !textView._recording)); -}); - -ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === 'bold') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleBold(); - else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; -}); - -ScriptingGlobals.add(function toggleUnderline(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === 'underline') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleUnderline(); - else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; -}); - -ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === 'italics') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleItalics(); - else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; + 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() { @@ -832,62 +770,39 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); // toggle: Set overlay status of selected document -ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - if (checkResult) { - if (selected?.type === DocumentType.INK) { - return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent'; - } - return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent'; - } - SetActiveIsInkMask(!ActiveIsInkMask()); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.isInkMask = !doc.isInkMask)); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) { +ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); - if (checkResult) { - if (selected?.type === DocumentType.INK) { - return StrCast(selected.fillColor); - } - return ActiveFillColor(); - } - SetActiveFillColor(StrCast(color)); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.fillColor = color)); -}); - -ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) { - if (checkResult) { - const selected = SelectionManager.Docs().lastElement(); - if (selected?.type === DocumentType.INK) { - return NumCast(selected.strokeWidth); - } - return ActiveInkWidth(); - } - SetActiveInkWidth(width.toString()); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.strokeWidth = Number(width))); -}); + // 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.isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'), + setInk: (doc: Doc) => (doc.isInkMask = !doc.isInkMask), + setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), + }], + ['fillColor', { + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'), + setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), + setMode: () => SetActiveFillColor(StrCast(value)), + }], + [ 'strokeWidth', { + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.strokeWidth) : ActiveInkWidth()), + setInk: (doc: Doc) => (doc.strokeWidth = NumCast(value)), + setMode: () => SetActiveInkWidth(value.toString()), + }], + ['strokeColor', { + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.color) : ActiveInkColor()), + setInk: (doc: Doc) => (doc.color = String(value)), + setMode: () => SetActiveInkColor(StrCast(value)), + }], + ]); -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boolean) { if (checkResult) { - const selected = SelectionManager.Docs().lastElement(); - if (selected?.type === DocumentType.INK) { - return StrCast(selected.color); - } - return ActiveInkColor(); + return map.get(option)?.checkResult(); } - SetActiveInkColor(StrCast(color)); + map.get(option)?.setMode(); SelectionManager.Docs() .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.color = String(color))); + .map(doc => map.get(option)?.setInk(doc)); }); /** WEB diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index fcd6e0c55..aa269d8d6 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -32,7 +32,7 @@ export class DashDocCommentView { }; this.root = ReactDOM.createRoot(this.dom); - this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />); (this as any).dom = this.dom; } @@ -48,7 +48,7 @@ export class DashDocCommentView { } interface IDashDocCommentViewInternal { - docid: string; + docId: string; view: any; getPos: any; } @@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV } onPointerLeaveCollapsed(e: any) { - DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); + DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); e.preventDefault(); e.stopPropagation(); } onPointerEnterCollapsed(e: any) { - DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); + DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); e.preventDefault(); e.stopPropagation(); } @@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { - expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) {} @@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV const state = this.props.view.state; for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { const m = state.doc.nodeAt(i); - if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) { + if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) { return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean }; } } - const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' }); + const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' }); this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc)); setTimeout(() => { try { @@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV return ( <span className="formattedTextBox-inlineComment" - id={'DashDocCommentView-' + this.props.docid} + id={'DashDocCommentView-' + this.props.docId} onPointerLeave={this.onPointerLeaveCollapsed} onPointerEnter={this.onPointerEnterCollapsed} onPointerUp={this.onPointerUpCollapsed} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 19ee1fc7b..61345f891 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -42,7 +42,7 @@ export class DashDocView { this.root = ReactDOM.createRoot(this.dom); this.root.render( - <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> + <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> ); } destroy() { @@ -53,7 +53,7 @@ export class DashDocView { } interface IDashDocViewInternal { - docid: string; + docId: string; alias: string; tbox: FormattedTextBox; width: string; @@ -77,7 +77,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; - this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); + this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); if (this._finalLayout) { if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { @@ -107,12 +107,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { super(props); this._textBox = this.props.tbox; - DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => { + DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { this.props.alias && - DocServer.GetRefField(this.props.docid).then(async dashDocBase => { + DocServer.GetRefField(this.props.docId).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias); + const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias); aliasedDoc.layoutKey = 'layout'; this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined); this.updateDoc(aliasedDoc); @@ -161,12 +161,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { }; onPointerLeave = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = ''); }; onPointerEnter = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = 'orange'); }; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index b33529aeb..21ccf3bc7 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,5 +1,8 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; +import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; @@ -7,18 +10,14 @@ import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { ComputedField } from '../../../../fields/ScriptField'; import { Cast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; import React = require('react'); -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; -import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; -import { Tooltip } from '@material-ui/core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CollectionViewType } from '../../../documents/DocumentTypes'; -import { NodeSelection } from 'prosemirror-state'; -import { OpenWhere } from '../DocumentView'; -import { FormattedTextBoxComment } from './FormattedTextBoxComment'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -55,7 +54,7 @@ export class DashFieldView { unclickable={this.unclickable} getPos={getPos} fieldKey={node.attrs.fieldKey} - docid={node.attrs.docid} + docId={node.attrs.docId} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} @@ -77,7 +76,7 @@ export class DashFieldView { interface IDashFieldViewInternal { fieldKey: string; - docid: string; + docId: string; hideKey: boolean; tbox: FormattedTextBox; width: number; @@ -101,8 +100,12 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._fieldKey = this.props.fieldKey; this._textBoxDoc = this.props.tbox.props.Document; - if (this.props.docid) { - DocServer.GetRefField(this.props.docid).then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + if (this.props.docId) { + DocServer.GetRefField(this.props.docId).then( + action(async dashDoc => { + dashDoc instanceof Doc && (this._dashDoc = dashDoc); + }) + ); } else { this._dashDoc = this.props.tbox.rootDoc; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d857b150c..a2c1195cb 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -11,7 +11,7 @@ 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, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -48,6 +48,7 @@ import { StyleProp } from '../../StyleProvider'; import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; +import { PinProps, PresBox } from '../trails'; import { DashDocCommentView } from './DashDocCommentView'; import { DashDocView } from './DashDocView'; import { DashFieldView } from './DashFieldView'; @@ -63,7 +64,6 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { PinProps, PresBox } from '../trails'; const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; @@ -186,7 +186,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return ''; } public static GetDocFromUrl(url: string) { - return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid + return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } constructor(props: any) { @@ -363,7 +363,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let linkTime; let linkAnchor; let link; - DocListCast(this.dataDoc.links).forEach((l, i) => { + LinkManager.Links(this.dataDoc).forEach((l, i) => { const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); @@ -400,7 +400,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.linkRelationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; @@ -453,7 +453,7 @@ 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 ?? - (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || + (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; @@ -545,7 +545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps width: target[WidthSym](), height: target[HeightSym](), title: 'dashDoc', - docid: target[Id], + docId: target[Id], float: 'unset', }); if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) { @@ -687,7 +687,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @undoBatch deleteAnnotation = (anchor: Doc) => { - LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]); + 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); @@ -1014,7 +1014,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } componentDidMount() { !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - this._cachedLinks = DocListCast(this.Document.links); + this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); this._disposers.autoHeight = reaction( () => this.autoHeight, @@ -1041,7 +1041,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps { fireImmediately: true } ); this._disposers.links = reaction( - () => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks + () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); this._cachedLinks = newLinks; @@ -1260,7 +1260,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ - view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ + view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], location: 'add:right', @@ -1435,7 +1435,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } else (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); - this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. + this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. if ((e.target as any).tagName === 'AUDIOTAG') { e.preventDefault(); e.stopPropagation(); @@ -1485,7 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; - if (!state || !editor) return; + if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1669,8 +1669,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey) { + if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); + e.stopPropagation(); + this.props.setTitleFocus?.(); return; } const state = this._editorView!.state; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 5675776fb..9e9b61db3 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -89,8 +89,8 @@ export class RichTextRules { textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation const node = (state.doc.resolve(start) as any).nodeAfter; - const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] }); - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' }); + const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); const sm = state.storedMarks || undefined; const replaced = node ? state.tr @@ -248,17 +248,17 @@ export class RichTextRules { // [[fieldKey:Doc]] => 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 docId = match[3]?.replace(':', ''); const value = match[2]?.substring(1); if (!fieldKey) { - if (docid) { - DocServer.GetRefField(docid).then(docx => { + if (docId) { + DocServer.GetRefField(docId).then(docx => { const rstate = this.TextBox.EditorView?.state; const selection = rstate?.selection.$from.pos; if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); + const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId); DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined); const fstate = this.TextBox.EditorView?.state; @@ -274,7 +274,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false }); + 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); }), @@ -311,16 +311,16 @@ export class RichTextRules { const fieldKey = match[1] || ''; const fieldParam = match[2]?.replace('…', '...') || ''; const rawdocid = match[3]?.substring(1); - const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined; - if (!fieldKey && !docid) return state.tr; - docid && - DocServer.GetRefField(docid).then(docx => { + const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined; + if (!fieldKey && !docId) return state.tr; + docId && + DocServer.GetRefField(docId).then(docx => { if (!(docx instanceof Doc && docx)) { - Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid); + Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId); } }); const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() }); const sm = state.storedMarks || undefined; return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; }), diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index aa2475dca..6c9d5d31a 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = { dashComment: { attrs: { - docid: { default: '' }, + docId: { default: '' }, }, inline: true, group: 'inline', @@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = { title: { default: null }, float: { default: 'left' }, location: { default: 'add:right' }, - docid: { default: '' }, + docId: { default: '' }, }, group: 'inline', draggable: true, @@ -246,7 +246,7 @@ export const nodes: { [index: string]: NodeSpec } = { float: { default: 'right' }, hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off fieldKey: { default: '' }, - docid: { default: '' }, + docId: { default: '' }, alias: { default: '' }, }, group: 'inline', @@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { fieldKey: { default: '' }, - docid: { default: '' }, + docId: { default: '' }, hideKey: { default: false }, editable: { default: true }, }, diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 94962650a..e79e7472a 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,13 +3,13 @@ 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 { ColorState, SketchPicker } from 'react-color'; -import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { AudioField } from '../../../../fields/URLField'; import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; @@ -19,6 +19,7 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp import { DocumentManager } from '../../../util/DocumentManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; +import { SerializationHelper } from '../../../util/SerializationHelper'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; @@ -34,7 +35,6 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -import { SerializationHelper } from '../../../util/SerializationHelper'; const { Howl } = require('howler'); export interface pinDataTypes { @@ -69,9 +69,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } + static navigateToDocScript: ScriptField; + + constructor(props: any) { + super(props); + if (!PresBox.navigateToDocScript) { + PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!; + } + } private _disposers: { [name: string]: IReactionDisposer } = {}; public selectedArray = new ObservableSet<Doc>(); + _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves + _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. + _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things @observable public static Instance: PresBox; @@ -87,6 +98,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _newDocumentTools: boolean = false; @observable _openMovementDropdown: boolean = false; @observable _openEffectDropdown: boolean = false; + @observable _openBulletEffectDropdown: boolean = false; @observable _presentTools: boolean = false; @observable _treeViewMap: Map<Doc, number> = new Map(); @observable _presKeyEvents: boolean = false; @@ -115,6 +127,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } + public static targetRenderedDoc = (doc: Doc) => { + const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null); + return targetDoc?.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; return false; @@ -138,14 +154,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); - _unmounting = false; @action componentWillUnmount() { this._unmounting = true; if (this._presTimer) clearTimeout(this._presTimer); document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); this.resetPresentation(); - // Turn of progressivize editors this.turnOffEdit(true); Object.values(this._disposers).forEach(disposer => disposer?.()); } @@ -185,6 +199,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => SelectionManager.Views(), views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation() ); + this._disposers.editing = reaction( + () => this.layoutDoc.presStatus === PresStatus.Edit, + editing => { + if (editing) { + this.childDocs.forEach(doc => { + if (doc.presIndexed !== undefined) { + this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined)); + doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1); + } + }); + } + } + ); } @action @@ -204,7 +231,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; stopTempMedia = (targetDocField: FieldResult) => { - const targetDoc = Cast(targetDocField, Doc, null); + const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField); if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); targMedia?.ComponentView?.Pause?.(); @@ -217,7 +244,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { nextSlide = (slideNum?: number) => { const nextSlideInd = slideNum ?? this.itemIndex + 1; let curSlideInd = nextSlideInd; - CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); + //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.()); this.clearSelectedArray(); const doGroupWithUp = (nextSelected: number, force = false) => @@ -243,18 +270,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doGroupWithUp(curSlideInd, true)(); }; + // docs within a slide target that will be progressively revealed + progressivizedItems = (doc: Doc) => { + 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); + } + }; // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { + const progressiveReveal = (first: boolean) => { + const presIndexed = Cast(this.activeItem?.presIndexed, 'number', null); + if (presIndexed !== undefined) { + const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem); + targetRenderedDoc._dataTransition = 'all 1s'; + targetRenderedDoc.opacity = 1; + setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000); + const listItems = this.progressivizedItems(this.activeItem); + if (listItems && presIndexed < listItems.length) { + if (!first) { + const listItemDoc = listItems[presIndexed]; + const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc); + Doc.linkFollowUnhighlight(); + Doc.HighlightDoc(listItemDoc); + listItemDoc.presEffect = this.activeItem.presBulletEffect; + listItemDoc.presTransition = 500; + targetView?.setAnimEffect(listItemDoc, 500); + if (targetView?.docView && this.activeItem.presBulletExpand) { + targetView.docView._animateScalingTo = 1.1; + Doc.AddUnHighlightWatcher(() => (targetView!.docView!._animateScalingTo = 0)); + } + listItemDoc.opacity = undefined; + this.activeItem.presIndexed = presIndexed + 1; + } + return true; + } + } + }; + if (progressiveReveal(false)) return true; if (this.childDocs[this.itemIndex + 1] !== undefined) { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]); const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); + progressiveReveal(true); // shows first progressive document, but without a transition effect } else { if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) { // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); + progressiveReveal(true); // shows first progressive document, but without a transition effect } return 0; } @@ -291,21 +357,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; - if (activeFrame !== undefined) { - const transTime = NumCast(activeItem.presTransition, 500); - const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); - const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; - if (context) { - const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView; - if (ffview?.childDocs) { - this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, ffview.childDocs, transTime); - context._currentFrame = NumCast(activeFrame); - } - } - } if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } @@ -313,14 +364,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.stopTempMedia(from.presentationTargetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played - if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') { - this.startTempMedia(targetDoc, activeItem); + if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') { + this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array this.turnOffEdit(); this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list - this.onHideDocument(); //Handles hide after/before + this.doHideBeforeAfter(); //Handles hide after/before } }); static pinDataTypes(target?: Doc): pinDataTypes { @@ -338,7 +389,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, viewType, pivot, filters, temporal, clippable, dataview, textview, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, viewType, pivot, filters, temporal, clippable, dataview, datarange, textview, poslayoutview, dataannos }; } @action @@ -366,6 +417,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + + const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; + if (activeFrame !== undefined) { + const transTime = NumCast(activeItem.presTransition, 500); + const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); + const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; + if (context) { + const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + if (ffview?.childDocs) { + PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime); + ffview.rootDoc._currentFrame = NumCast(activeFrame); + } + } + } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) { if (bestTarget.xRange !== activeItem.presXRange) { bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy](); @@ -491,7 +556,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10); } - if (pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) { + if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) { const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; @@ -505,9 +570,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } else { if (bestTarget._panX !== activeItem.presPanX || bestTarget._panY !== activeItem.presPanY || bestTarget._viewScale !== activeItem.presViewScale) { - bestTarget._panX = activeItem.presPanX; - bestTarget._panY = activeItem.presPanY; - bestTarget._viewScale = activeItem.presViewScale; + bestTarget._panX = activeItem.presPanX ?? bestTarget._panX; + bestTarget._panY = activeItem.presPanY ?? bestTarget._panY; + bestTarget._viewScale = activeItem.presViewScale ?? bestTarget._viewScale; changed = true; } } @@ -652,7 +717,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, - originatingDoc: activeItem, + openLocation: OpenWhere.addLeft, + anchorDoc: activeItem, easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, zoomTextSelections: BoolCast(activeItem.presZoomText), playAudio: BoolCast(activeItem.presPlayAudio), @@ -681,22 +747,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * they are hidden each time the presentation is updated. */ @action - onHideDocument = () => { + doHideBeforeAfter = () => { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); - const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + const tagDoc = PresBox.targetRenderedDoc(curDoc); const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); + let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined; if (curDoc.presHide) { if (index !== this.itemIndex) { - tagDoc.opacity = 1; + opacity = 1; } } const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); if (curDoc.presHideBefore && index === hidingIndBef) { if (index > this.itemIndex) { - tagDoc.opacity = 0; + opacity = 0; } else if (index === this.itemIndex || !curDoc.presHideAfter) { - tagDoc.opacity = 1; + opacity = 1; + setTimeout(() => (tagDoc._dataTransition = undefined), 1000); } } const hidingIndAft = @@ -706,17 +774,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); if (curDoc.presHideAfter && index === hidingIndAft) { if (index < this.itemIndex) { - tagDoc.opacity = 0; + opacity = 0; } else if (index === this.itemIndex || !curDoc.presHideBefore) { - tagDoc.opacity = 1; + opacity = 1; } } const hidingInd = itemIndexes.find(item => item === this.itemIndex); if (curDoc.presHide && index === hidingInd) { if (index === this.itemIndex) { - tagDoc.opacity = 0; + opacity = 0; } } + opacity !== undefined && (tagDoc.opacity = opacity); }); }; @@ -780,7 +849,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //stops the presentaton. resetPresentation = () => { this.childDocs - .map(doc => Cast(doc.presentationTargetDoc, Doc, null)) + .map(doc => PresBox.targetRenderedDoc(doc)) .filter(doc => doc instanceof Doc) .forEach(doc => { try { @@ -806,6 +875,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); }; + initializePresState = (startIndex: number) => { + this.childDocs.forEach((doc, index) => { + const tagDoc = PresBox.targetRenderedDoc(doc); + if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0; + if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0; + if (doc.presIndexed !== undefined && index >= startIndex) { + const startInd = NumCast(doc.presIndexedStart); + this.progressivizedItems(doc) + ?.slice(startInd) + .forEach(indexedDoc => (indexedDoc.opacity = 0)); + doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd); + } + // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; + }); + }; + /** * The function that starts the presentation at the given index, also checking if actions should be applied * directly at start. @@ -817,12 +902,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { clearTimeout(this._presTimer); if (this.childDocs.length) { this.layoutDoc.presStatus = PresStatus.Autoplay; - this.childDocs.forEach(doc => { - const tagDoc = doc.presentationTargetDoc as Doc; - if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) tagDoc.opacity = 0; - if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) tagDoc.opacity = 0; - // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; - }); + this.initializePresState(startIndex); const func = () => { const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition); this._presTimer = setTimeout(() => { @@ -859,6 +939,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc._height = 30; doc._width = PresBox.minimizedWidth; Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + PresBox.Instance.initializePresState(PresBox.Instance.itemIndex); return (doc.presStatus = PresStatus.Manual); } @@ -1267,12 +1348,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (timeInMS > 100000) timeInMS = 100000; setter(timeInMS); }; - setTransitionTime = (number: String, change?: number) => { + + @undoBatch + updateTransitionTime = (number: String, change?: number) => { PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change); }; // Converts seconds to ms and updates presTransition - setZoom = (number: String, change?: number) => { + @undoBatch + updateZoom = (number: String, change?: number) => { let scale = Number(number) / 100; if (change) scale += change; if (scale < 0.01) scale = 0.01; @@ -1280,8 +1364,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectedArray.forEach(doc => (doc.presZoom = scale)); }; - // Converts seconds to ms and updates presDuration - setDurationTime = (number: String, change?: number) => { + /* + * Converts seconds to ms and updates presDuration + */ + @undoBatch + updateDurationTime = (number: String, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; @@ -1289,9 +1376,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.selectedArray.forEach(doc => (doc.presDuration = timeInMS)); }; - /** - * When the movement dropdown is changes - */ @undoBatch updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement))); @@ -1322,6 +1406,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox; this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox)); }; + @undoBatch @action updateEaseFunc = (activeItem: Doc) => { @@ -1335,12 +1420,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action - updateEffect = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffect = effect)); - - _batch: UndoManager.Batch | undefined = undefined; + updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presEffect = effect))); + static _sliderBatch: any; public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { - let batch: any; return ( <input type="range" @@ -1351,10 +1434,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { - batch = UndoManager.StartBatch('pres slider'); + PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); e.stopPropagation(); }} - onPointerUp={() => batch?.end()} + onPointerUp={() => PresBox._sliderBatch.end()} onChange={e => { e.stopPropagation(); change(e.target.value); @@ -1362,11 +1445,161 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ); }; + + @undoBatch + @action + applyTo = (array: Doc[]) => { + this.updateMovement(this.activeItem.presMovement as PresMovement, true); + this.updateEffect(this.activeItem.presEffect as PresEffect, false, true); + this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); + this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true); + const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem; + array.forEach(curDoc => { + curDoc.presTransition = presTransition; + curDoc.presDuration = presDuration; + curDoc.presHideBefore = presHideBefore; + curDoc.presHideAfter = presHideAfter; + }); + }; + + @computed get visibiltyDurationDropdown() { + const activeItem = this.activeItem; + if (activeItem && this.targetDoc) { + const targetType = this.targetDoc.type; + let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0; + if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); + return ( + <div className="presBox-ribbon"> + <div className="ribbon-doubleButton"> + <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> + <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}> + Hide before + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> + <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}> + Hide + </div> + </Tooltip> + + <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> + <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> + Hide after + </div> + </Tooltip> + + <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}> + Lightbox + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}> + <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}> + {`${StrCast(activeItem.presEaseFunc, 'ease')}`} + </div> + </Tooltip> + </div> + {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : ( + <> + <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 + </div> + <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> + <FontAwesomeIcon icon={'caret-up'} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}> + <FontAwesomeIcon icon={'caret-down'} /> + </div> + </div> + </div> + {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)} + <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> + <div className="slider-text">Short</div> + <div className="slider-text">Medium</div> + <div className="slider-text">Long</div> + </div> + </> + )} + </div> + ); + } + } + @computed get progressivizeDropdown() { + const activeItem = this.activeItem; + if (activeItem && this.targetDoc) { + const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None; + const bulletEffect = (effect: PresEffect) => ( + <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, true)}> + {effect} + </div> + ); + return ( + <div className="presBox-ribbon"> + <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Progressivize Collection</div> + <input + className="presBox-checkbox" + style={{ margin: 10 }} + type="checkbox" + onChange={() => { + activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined; + activeItem.presHideBefore = activeItem.presIndexed !== undefined; + const tagDoc = PresBox.targetRenderedDoc(this.activeItem); + const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; + activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0; + // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. + // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. + let dataField = Doc.LayoutFieldKey(tagDoc); + if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '-annotations'; + + if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`); + else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`); + }} + checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false} + /> + </div> + <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Progressivize First Bullet</div> + <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presIndexedStart = activeItem.presIndexedStart ? 0 : 1)} checked={!NumCast(activeItem.presIndexedStart)} /> + </div> + <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Expand Current Bullet</div> + <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} /> + </div> + + <div className="ribbon-box"> + Bullet Effect + <div + className="presBox-dropdown" + onClick={action(e => { + e.stopPropagation(); + this._openBulletEffectDropdown = !this._openBulletEffectDropdown; + })} + style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> + {effect?.toString()} + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> + {bulletEffect(PresEffect.None)} + {bulletEffect(PresEffect.Fade)} + {bulletEffect(PresEffect.Flip)} + {bulletEffect(PresEffect.Rotate)} + {bulletEffect(PresEffect.Bounce)} + {bulletEffect(PresEffect.Roll)} + </div> + </div> + </div> + </div> + ); + } + return null; + } @computed get transitionDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; + const activeItem = this.activeItem; const presEffect = (effect: PresEffect) => ( - <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}> + <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}> {effect} </div> ); @@ -1387,14 +1620,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); }; - if (activeItem && targetDoc) { - const type = targetDoc.type; + if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; const zoom = NumCast(activeItem.presZoom, 1) * 100; - let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0; - if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None; - // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom; return ( <div className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`} @@ -1404,6 +1633,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { e.stopPropagation(); this._openMovementDropdown = false; this._openEffectDropdown = false; + this._openBulletEffectDropdown = false; })}> <div className="ribbon-box"> Movement @@ -1427,33 +1657,33 @@ 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={action(e => this.setZoom(e.target.value))} />% + <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> <div className="ribbon-propertyUpDown"> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> <FontAwesomeIcon icon={'caret-up'} /> </div> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> <FontAwesomeIcon icon={'caret-down'} /> </div> </div> </div> - {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)} + {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)} <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Transition Speed</div> + <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.setTransitionTime(e.target.value))} /> s + <input className="presBox-input" type="number" 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={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> <FontAwesomeIcon icon={'caret-up'} /> </div> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}> <FontAwesomeIcon icon={'caret-down'} /> </div> </div> </div> - {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.setTransitionTime)} + {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)} <div className={'slider-headers'}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> @@ -1461,62 +1691,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> <div className="ribbon-box"> - Visibility {'&'} Duration - <div className="ribbon-doubleButton"> - <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}> - Hide before - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}> - Hide - </div> - </Tooltip> - - <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> - <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> - Hide after - </div> - </Tooltip> - - <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}> - Lightbox - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}> - <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}> - {`${StrCast(activeItem.presEaseFunc, 'ease')}`} - </div> - </Tooltip> - </div> - {type === DocumentType.AUDIO || type === DocumentType.VID ? null : ( - <> - <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={action(e => this.setDurationTime(e.target.value))} /> s - </div> - <div className="ribbon-propertyUpDown"> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}> - <FontAwesomeIcon icon={'caret-up'} /> - </div> - <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}> - <FontAwesomeIcon icon={'caret-down'} /> - </div> - </div> - </div> - {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)} - <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}> - <div className="slider-text">Short</div> - <div className="slider-text">Medium</div> - <div className="slider-text">Long</div> - </div> - </> - )} - </div> - <div className="ribbon-box"> Effects <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Play Audio Annotation</div> @@ -1565,173 +1739,154 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } } - - @undoBatch - @action - applyTo = (array: Doc[]) => { - this.updateMovement(this.activeItem.presMovement as PresMovement, true); - this.updateEffect(this.activeItem.presEffect as PresEffect, true); - this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true); - const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem; - array.forEach(curDoc => { - curDoc.presTransition = presTransition; - curDoc.presDuration = presDuration; - curDoc.presHideBefore = presHideBefore; - curDoc.presHideAfter = presHideAfter; - }); - }; - @computed get mediaOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const clipStart: number = NumCast(activeItem.clipStart); - const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration'])); - const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); - if (activeItem && targetDoc) { + const activeItem = this.activeItem; + if (activeItem && this.targetDoc) { + const clipStart = NumCast(activeItem.clipStart); + const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration'])); return ( - <div> - <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div> - <div className="ribbon-box"> - Start {'&'} End Time - <div className={'slider-headers'}> - <div className="slider-block"> - <div className="slider-text" style={{ fontWeight: 500 }}> - Start time (s) - </div> - <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> - <input - className="presBox-input" - style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} - type="number" - value={NumCast(activeItem.presStartTime).toFixed(2)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.presStartTime = Number(e.target.value); - })} - /> - </div> - </div> - <div className="slider-block"> - <div className="slider-text" style={{ fontWeight: 500 }}> - Duration (s) - </div> - <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> - {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10} - </div> + <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> + <div> + <div className="ribbon-box"> + Start {'&'} End Time + <div className={'slider-headers'}> + <div className="slider-block"> + <div className="slider-text" style={{ fontWeight: 500 }}> + Start time (s) </div> - <div className="slider-block"> - <div className="slider-text" style={{ fontWeight: 500 }}> - End time (s) - </div> - <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> - <input - className="presBox-input" - onKeyDown={e => e.stopPropagation()} - style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} - type="number" - value={NumCast(activeItem.presEndTime).toFixed(2)} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - activeItem.presEndTime = Number(e.target.value); - })} - /> - </div> + <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <input + className="presBox-input" + style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} + type="number" + value={NumCast(activeItem.presStartTime).toFixed(2)} + onKeyDown={e => e.stopPropagation()} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { + activeItem.presStartTime = Number(e.target.value); + })} + /> </div> </div> - <div className="multiThumb-slider"> - <input - type="range" - step="0.1" - min={clipStart} - max={clipEnd} - value={NumCast(activeItem.presEndTime)} - style={{ gridColumn: 1, gridRow: 1 }} - className={`toolbar-slider ${'end'}`} - id="toolbar-slider" - onPointerDown={e => { - this._batch = UndoManager.StartBatch('presEndTime'); - const endBlock = document.getElementById('endTime'); - if (endBlock) { - endBlock.style.color = Colors.LIGHT_GRAY; - endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; - } - e.stopPropagation(); - }} - onPointerUp={() => { - this._batch?.end(); - const endBlock = document.getElementById('endTime'); - if (endBlock) { - endBlock.style.color = Colors.BLACK; - endBlock.style.backgroundColor = Colors.LIGHT_GRAY; - } - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - e.stopPropagation(); - activeItem.presEndTime = Number(e.target.value); - }} - /> - <input - type="range" - step="0.1" - min={clipStart} - max={clipEnd} - value={NumCast(activeItem.presStartTime)} - style={{ gridColumn: 1, gridRow: 1 }} - className={`toolbar-slider ${'start'}`} - id="toolbar-slider" - onPointerDown={e => { - this._batch = UndoManager.StartBatch('presStartTime'); - const startBlock = document.getElementById('startTime'); - if (startBlock) { - startBlock.style.color = Colors.LIGHT_GRAY; - startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; - } - e.stopPropagation(); - }} - onPointerUp={() => { - this._batch?.end(); - const startBlock = document.getElementById('startTime'); - if (startBlock) { - startBlock.style.color = Colors.BLACK; - startBlock.style.backgroundColor = Colors.LIGHT_GRAY; - } - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - e.stopPropagation(); - activeItem.presStartTime = Number(e.target.value); - }} - /> - </div> - <div className="slider-headers"> - <div className="slider-text">{clipStart.toFixed(2)} s</div> - <div className="slider-text"></div> - <div className="slider-text">{clipEnd.toFixed(2)} s</div> - </div> - </div> - <div className="ribbon-final-box"> - Playback - <div className="presBox-subheading">Start playing:</div> - <div className="presBox-radioButtons"> - <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} /> - <div>On click</div> + <div className="slider-block"> + <div className="slider-text" style={{ fontWeight: 500 }}> + Duration (s) </div> - <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} /> - <div>Automatically</div> + <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> + {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10} </div> </div> - <div className="presBox-subheading">Stop playing:</div> - <div className="presBox-radioButtons"> - <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} /> - <div>At audio end time</div> + <div className="slider-block"> + <div className="slider-text" style={{ fontWeight: 500 }}> + End time (s) </div> - <div className="checkbox-container"> - <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} /> - <div>On slide change</div> + <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> + <input + className="presBox-input" + onKeyDown={e => e.stopPropagation()} + style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} + type="number" + value={NumCast(activeItem.presEndTime).toFixed(2)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { + activeItem.presEndTime = Number(e.target.value); + })} + /> </div> - {/* <div className="checkbox-container"> + </div> + </div> + <div className="multiThumb-slider"> + <input + type="range" + step="0.1" + min={clipStart} + max={clipEnd} + value={NumCast(activeItem.presEndTime)} + style={{ gridColumn: 1, gridRow: 1 }} + className={`toolbar-slider ${'end'}`} + id="toolbar-slider" + onPointerDown={e => { + this._batch = UndoManager.StartBatch('presEndTime'); + const endBlock = document.getElementById('endTime'); + if (endBlock) { + endBlock.style.color = Colors.LIGHT_GRAY; + endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + } + e.stopPropagation(); + }} + onPointerUp={() => { + this._batch?.end(); + const endBlock = document.getElementById('endTime'); + if (endBlock) { + endBlock.style.color = Colors.BLACK; + endBlock.style.backgroundColor = Colors.LIGHT_GRAY; + } + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + activeItem.presEndTime = Number(e.target.value); + }} + /> + <input + type="range" + step="0.1" + min={clipStart} + max={clipEnd} + value={NumCast(activeItem.presStartTime)} + style={{ gridColumn: 1, gridRow: 1 }} + className={`toolbar-slider ${'start'}`} + id="toolbar-slider" + onPointerDown={e => { + this._batch = UndoManager.StartBatch('presStartTime'); + const startBlock = document.getElementById('startTime'); + if (startBlock) { + startBlock.style.color = Colors.LIGHT_GRAY; + startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; + } + e.stopPropagation(); + }} + onPointerUp={() => { + this._batch?.end(); + const startBlock = document.getElementById('startTime'); + if (startBlock) { + startBlock.style.color = Colors.BLACK; + startBlock.style.backgroundColor = Colors.LIGHT_GRAY; + } + }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + activeItem.presStartTime = Number(e.target.value); + }} + /> + </div> + <div className="slider-headers"> + <div className="slider-text">{clipStart.toFixed(2)} s</div> + <div className="slider-text"></div> + <div className="slider-text">{clipEnd.toFixed(2)} s</div> + </div> + </div> + <div className="ribbon-final-box"> + Playback + <div className="presBox-subheading">Start playing:</div> + <div className="presBox-radioButtons"> + <div className="checkbox-container"> + <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} /> + <div>On click</div> + </div> + <div className="checkbox-container"> + <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} /> + <div>Automatically</div> + </div> + </div> + <div className="presBox-subheading">Stop playing:</div> + <div className="presBox-radioButtons"> + <div className="checkbox-container"> + <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} /> + <div>At audio end time</div> + </div> + <div className="checkbox-container"> + <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} /> + <div>On slide change</div> + </div> + {/* <div className="checkbox-container"> <input className="presBox-checkbox" type="checkbox" onChange={() => activeItem.mediaStop = "afterSlide"} @@ -1748,7 +1903,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </select> </div> </div> */} - </div> </div> </div> </div> @@ -1756,7 +1910,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } } - @computed get newDocumentToolbarDropdown() { return ( <div @@ -1951,6 +2104,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={undoBatch( action(() => { this.layoutDoc.presStatus = 'manual'; + this.initializePresState(this.itemIndex); this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }) @@ -1961,65 +2115,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } - _keyTimer: NodeJS.Timeout | undefined; - - /** - * Returns the collection type as a string for headers - */ - @computed get stringType() { - if (this.activeItem) { - // prettier-ignore - switch (this.targetDoc.type) { - case DocumentType.PDF: return 'PDF'; - case DocumentType.RTF: return 'Text node'; - case DocumentType.COL: return 'Collection'; - case DocumentType.AUDIO: return 'Audio'; - case DocumentType.VID: return 'Video'; - case DocumentType.IMG: return 'Image'; - case DocumentType.WEB: return 'Web page'; - case DocumentType.MAP: return 'Map'; - default: return 'Other node'; - } - } - return ''; - } - - @observable private openActiveColorPicker: boolean = false; - @observable private openViewedColorPicker: boolean = false; - - @undoBatch - @action - switchActive = (color: ColorState) => { - this.targetDoc['pres-text-color'] = String(color.hex); - return true; - }; - @undoBatch - @action - switchPresented = (color: ColorState) => { - this.targetDoc['pres-text-viewed-color'] = String(color.hex); - return true; - }; - - @computed get activeColorPicker() { - return !this.openActiveColorPicker ? null : ( - <SketchPicker - onChange={this.switchActive} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(this.targetDoc['pres-text-color'])} - /> - ); - } - - @computed get viewedColorPicker() { - return !this.openViewedColorPicker ? null : ( - <SketchPicker - onChange={this.switchPresented} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(this.targetDoc['pres-text-viewed-color'])} - /> - ); - } - @action turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths @@ -2056,14 +2151,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {isMini ? null : ( <> <div className="toolbar-divider" /> - {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}> - <div className={"toolbar-button"} - style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }} - onClick={this.toggleExpandMode}> - <FontAwesomeIcon icon={"eye"} /> - </div> - </Tooltip> - <div className="toolbar-divider" /> */} <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}> <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} /> @@ -2113,6 +2200,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={undoBatch(() => { if (this.childDocs.length) { this.layoutDoc.presStatus = 'manual'; + this.initializePresState(this.itemIndex); this.gotoDocument(this.itemIndex, this.activeItem); } })}> @@ -2137,7 +2225,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get playButtons() { - const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; + 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); // Case 1: There are still other frames and should go through all frames before going to next slide @@ -2225,7 +2313,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> - {`${inOverlay ? '' : 'Slide'} ${this.itemIndex + 1} / ${this.childDocs.length}`} + {inOverlay ? '' : 'Slide'} {this.itemIndex + 1} + {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> <div className="presPanel-divider"></div> {this.props.PanelWidth() > 250 ? ( @@ -2272,6 +2361,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.presStatus = PresStatus.Manual; } }; + @undoBatch @action exitClicked = () => { @@ -2279,7 +2369,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { clearTimeout(this._presTimer); }; - AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => { + AddToMap = (treeViewDoc: Doc, index: number[]) => { + if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. var indexNum = 0; for (let i = 0; i < index.length; i++) { indexNum += index[i] * 10 ** -i; @@ -2292,25 +2383,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs } } - return this.childDocs; }; - RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => { + RemFromMap = (treeViewDoc: Doc, index: number[]) => { + if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element. if (!this._unmounting && this.isTree) { this._treeViewMap.delete(treeViewDoc); this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap)); } - return this.childDocs; }; - // TODO: [AL] implement sort function for an array of numbers (e.g. arr[1,2,4] v arr[1,2,1]) sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]); render() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; + 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 @@ -2345,7 +2434,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> <div className="presPanel-button-text"> - Slide {this.itemIndex + 1} / {this.childDocs.length} + Slide {this.itemIndex + 1} + {this.activeItem.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length} </div> <div className="presPanel-divider" /> <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}> @@ -2372,6 +2462,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //childFitWidth={returnTrue} childOpacity={returnOne} //childLayoutString={PresElementBox.LayoutString('data')} + childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} filterAddDocument={this.addDocumentFilter} @@ -2399,11 +2490,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } - static NavigateToDoc(bestTarget: Doc, activeItem: Doc) { - PresBox.NavigateToTarget(bestTarget, activeItem); - } } ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) { - PresBox.NavigateToDoc(bestTarget, activeItem); + PresBox.NavigateToTarget(bestTarget, activeItem); }); diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 3b101a0c6..db6b1f011 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,5 +1,5 @@ import React = require('react'); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -7,10 +7,10 @@ import { List } from '../../../fields/List'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { LinkFollower } from '../../util/LinkFollower'; import { undoBatch } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { AnchorMenu } from './AnchorMenu'; import './Annotation.scss'; -import { OpenWhere } from '../nodes/DocumentView'; interface IAnnotationProps extends FieldViewProps { anno: Doc; @@ -82,7 +82,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { e.stopPropagation(); } else if (e.button === 0) { e.stopPropagation(); - LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false); + LinkFollower.FollowLink(undefined, this.annoTextRegion, false); } }; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 3ba60edd7..fe9b13fbe 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, StrCast } from '../../../fields/Types'; -import { StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -470,7 +469,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } } style={{ - fontWeight: DocListCast(fromDoc?.links).find( + fontWeight: LinkManager.Links(fromDoc).find( link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc) ) ? 'bold' |
