import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Toggle, ToggleType, Type } from '@dash/components'; import { Lambda, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { RichTextField } from '../../../fields/RichTextField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, undoable } from '../../util/UndoManager'; import { AntimodeMenu } from '../AntimodeMenu'; import { EditableView } from '../EditableView'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import './CollectionMenu.scss'; import { CollectionLinearView } from './collectionLinear'; import { SettingsManager } from '../../util/SettingsManager'; interface CollectionMenuProps { panelHeight: () => number; panelWidth: () => number; toggleTopBar: () => void; topBarHeight: () => number; togglePropertiesFlyout: () => void; } @observer export class CollectionMenu extends AntimodeMenu { // eslint-disable-next-line no-use-before-define @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined = undefined; private _docBtnRef = React.createRef(); constructor(props: CollectionMenuProps) { super(props); makeObservable(this); CollectionMenu.Instance = this; this._canFade = false; // don't let the inking menu fade away this.Pinned = BoolCast(Doc.UserDoc().menuCollections_pinned, true); this.jumpTo(300, 300); } componentDidMount() { reaction( () => DocumentView.Selected().lastElement(), view => view && this.SetSelection(view) ); } @action SetSelection(view: DocumentView) { this.SelectedCollection = view; } @action toggleMenuPin = () => { Doc.UserDoc().menuCollections_pinned = this.Pinned = !this.Pinned; if (!this.Pinned && this._left < 0) { this.jumpTo(300, 300); } }; buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); const { scale, translateX, translateY } = ClientUtils.GetScreenTransform(this._docBtnRef.current); return new Transform(-translateX, -translateY, 1 / scale); }; @computed get contMenuButtons() { const selDoc = Doc.MyContextMenuBtns; return !(selDoc instanceof Doc) ? null : (
); } render() { const headerIcon = this.props.topBarHeight() > 0 ? 'angle-double-up' : 'angle-double-down'; const headerTitle = this.props.topBarHeight() > 0 ? 'Close Header Bar' : 'Open Header Bar'; const propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; const propTitle = SnappingManager.PropertiesWidth > 0 ? 'Close Properties' : 'Open Properties'; const hardCodedButtons = (
0} icon={} tooltip={headerTitle} /> 0} icon={} tooltip={propTitle} />
); // dash col linear view buttons return (
{this.contMenuButtons} {hardCodedButtons}
); } } interface CollectionViewMenuProps { type: CollectionViewType; fieldKey: string; docView: DocumentView; } const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer export class CollectionViewBaseChrome extends React.Component { // (!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) get document() { return this.props.docView?.Document; } get target() { return this.document; } _templateCommand = { params: ['target', 'source'], title: 'item view', script: 'this.target.childLayoutTemplate = getDocTemplate(this.source?.[0])', immediate: undoable((source: Doc[]) => { let formatStr = source.length && Cast(source[0].text, RichTextField, null)?.Text; try { formatStr && JSON.parse(formatStr); } catch { formatStr = ''; } if (source.length === 1 && formatStr) { Doc.SetInPlace(this.target, 'childLayoutString', formatStr, false); } else if (source.length) { this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]); } else { Doc.SetInPlace(this.target, 'childLayoutString', undefined, true); Doc.SetInPlace(this.target, 'childLayoutTemplate', undefined, true); } }, ''), initialize: emptyFunction, }; _narrativeCommand = { params: ['target', 'source'], title: 'child click view', script: 'this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])', immediate: undoable((source: Doc[]) => { source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0])); }, 'narrative command'), initialize: emptyFunction, }; _contentCommand = { params: ['target', 'source'], title: 'set content', script: 'getProto(this.target).data = copyField(this.source);', immediate: undoable((source: Doc[]) => { this.target.$data = new List(source); }, ''), initialize: emptyFunction, }; _onClickCommand = { params: ['target', 'proxy'], title: 'copy onClick', script: `{ if (this.proxy?.[0]) { getProto(this.proxy[0]).onClick = copyField(this.target.onClick); getProto(this.proxy[0]).target = this.target.target; getProto(this.proxy[0]).source = copyField(this.target.source); }}`, immediate: undoable(() => {}, ''), initialize: emptyFunction, }; _viewCommand = { params: ['target'], title: 'bookmark view', script: "this.target._freeform_panX = this.target_freeform_panX; this.target._freeform_panY = this['target-freeform_panY']; this.target._freeform_scale = this['target_freeform_scale']; gotoFrame(this.target, this['target-currentFrame']);", immediate: undoable(() => { this.target._freeform_panX = 0; this.target._freeform_panY = 0; this.target._freeform_scale = 1; this.target._currentFrame = this.target._currentFrame === undefined ? undefined : 0; }, ''), initialize: (button: Doc) => { button['target-panX'] = this.target._freeform_panX; button['target-panY'] = this.target._freeform_panY; button['target-_ayout_scale'] = this.target._freeform_scale; button['target-currentFrame'] = this.target._currentFrame; }, }; _clusterCommand = { params: ['target'], title: 'fit content', script: 'this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox;', immediate: undoable(() => { this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox; }, ''), initialize: emptyFunction, }; _fitContentCommand = { params: ['target'], title: 'toggle clusters', script: 'this.target._freeform_useClusters = !this.target._freeform_useClusters;', immediate: undoable(() => { this.target._freeform_useClusters = !this.target._freeform_useClusters; }, ''), initialize: emptyFunction, }; _saveFilterCommand = { params: ['target'], title: 'save filter', script: `this.target._childFilters = compareLists(this.target_childFilters,this.target._childFilters) ? undefined : copyField(this.target_childFilters); this.target._searchFilterDocs = compareLists(this.target_searchFilterDocs,this.target._searchFilterDocs) ? undefined: copyField(this.target_searchFilterDocs);`, immediate: undoable(() => { this.target._childFilters = undefined; this.target._searchFilterDocs = undefined; }, ''), initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { button.target_childFilters = (Doc.MySearcher?._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher?._childFilters || activeDash._childFilters) as ObjectField) : undefined; button.target_searchFilterDocs = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs) : undefined; } }, }; @computed get _freeform_commands() { return Doc.noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } @computed get _stacking_commands() { return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _notetaking_commands() { return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _masonry_commands() { return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _schema_commands() { return Doc.noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } @computed get _doc_commands() { return Doc.noviceMode ? undefined : [this._onClickCommand]; } @computed get _tree_commands() { return undefined; } private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Freeform: return this._freeform_commands; case CollectionViewType.Tree: return this._tree_commands; case CollectionViewType.Schema: return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.NoteTaking: return this._notetaking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Time: return this._freeform_commands; case CollectionViewType.Carousel: return this._freeform_commands; case CollectionViewType.Carousel3D: return this._freeform_commands; default: return this._doc_commands; } } private _commandRef = React.createRef(); private _viewRef = React.createRef(); @observable private _currentKey: string = ''; componentDidMount = action(() => { this._currentKey = this._currentKey || (this._buttonizableCommands?.length ? this._buttonizableCommands[0]?.title : ''); }); commandChanged = (e: React.ChangeEvent) => { runInAction(() => { this._currentKey = e.target.selectedOptions[0].value; }); }; @action closeViewSpecs = () => { this.document._facetWidth = 0; }; private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer?.(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.document); } }; @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; if (docDragData?.draggedDocuments.length) { this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); return true; } return false; } dragViewDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, moveEv => { const vtype = this.props.type; const c = { params: ['target'], title: vtype, script: `this.target._type_collection = '${StrCast(this.props.type)}'`, immediate: (source: Doc[]) => { this.document._type_collection = Doc.getDocTemplate(source?.[0]); }, initialize: emptyFunction, }; DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY); return true; }, emptyFunction, emptyFunction ); }; dragCommandDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, moveEv => { this._buttonizableCommands ?.filter(c => c.title === this._currentKey) .map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.document }, c.params, c.initialize, moveEv.clientX, moveEv.clientY)); return true; }, emptyFunction, () => { this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate([])); } ); }; @computed get templateChrome() { return (
drop document to apply or drag to create button
} placement="bottom">
); } render() { return (
{!this._buttonizableCommands ? null : this.templateChrome}
); } } @observer export class CollectionNoteTakingViewChrome extends React.Component { @observable private _currentKey: string = ''; @observable private suggestions: string[] = []; get document() { return this.props.docView.Document; } @computed private get descending() { return StrCast(this.document._columnsSort) === 'descending'; } @computed get pivotField() { return StrCast(this.document._pivotField); } getKeySuggestions = async (value: string): Promise => { const val = value.toLowerCase(); const docs = DocListCast(this.document[this.props.fieldKey]); if (Doc.UserDoc().noviceMode) { if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } const keys = new Set(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); const noviceKeys = Array.from(keys).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().indexOf(val) > -1); } const keys = new Set(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); return Array.from(keys).filter(key => key.toLowerCase().indexOf(val) > -1); }; @action onKeyChange = (e: React.FormEvent, { newValue }: { newValue: string }) => { this._currentKey = newValue; }; getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) =>

{suggestion}

; onSuggestionFetch = async ({ value }: { value: string }) => { const sugg = await this.getKeySuggestions(value); runInAction(() => { this.suggestions = sugg; }); }; @action onSuggestionClear = () => { this.suggestions = []; }; @action setValue = (value: string) => { this.document._pivotField = value; return true; }; @action toggleSort = () => { this.document._columnsSort = this.document._columnsSort === 'descending' ? 'ascending' : this.document._columnsSort === 'ascending' ? undefined : 'descending'; }; @action resetValue = () => { this._currentKey = this.pivotField; }; render() { const doctype = this.props.docView.Document.type; const isPres: boolean = doctype === DocumentType.PRES; return isPres ? null : (
GROUP BY:
this.pivotField} autosuggestProps={{ resetValue: this.resetValue, value: this._currentKey, onChange: this.onKeyChange, autosuggestProps: { inputProps: { value: this._currentKey, onChange: this.onKeyChange, }, getSuggestionValue: this.getSuggestionValue, suggestions: this.suggestions, alwaysRenderSuggestions: true, renderSuggestion: this.renderSuggestion, onSuggestionsFetchRequested: this.onSuggestionFetch, onSuggestionsClearRequested: this.onSuggestionClear, }, }} oneLine SetValue={this.setValue} contents={this.pivotField ? this.pivotField : 'N/A'} />
); } } /** * Chrome for grid view. */ @observer export class CollectionGridViewChrome extends React.Component { private clicked: boolean = false; private entered: boolean = false; private decrementLimitReached: boolean = false; @observable private resize = false; private resizeListenerDisposer: Opt; get document() { return this.props.docView.Document; } @computed get panelWidth() { return this.props.docView.props.PanelWidth(); } componentDidMount() { runInAction(() => { this.resize = this.props.docView.props.PanelWidth() < 700; }); // listener to reduce text on chrome resize (panel resize) this.resizeListenerDisposer = reaction( () => this.panelWidth, newValue => { this.resize = newValue < 700; } ); } componentWillUnmount() { this.resizeListenerDisposer?.(); } get numCols() { return NumCast(this.document.gridNumCols, 10); } /** * Sets the value of `numCols` on the grid's Document to the value entered. */ onNumColsChange = (e: React.ChangeEvent) => { if (e.currentTarget.valueAsNumber > 0) undoable(() => { this.document.gridNumCols = e.currentTarget.valueAsNumber; }, '')(); }; /** * Sets the value of `rowHeight` on the grid's Document to the value entered. */ // @undoBatch // onRowHeightEnter = (e: React.KeyboardEvent) => { // if (e.key === "Enter" || e.key === "Tab") { // if (e.currentTarget.valueAsNumber > 0 && this.document.rowHeight as number !== e.currentTarget.valueAsNumber) { // this.document.rowHeight = e.currentTarget.valueAsNumber; // } // } // } /** * Sets whether the grid is flexible or not on the grid's Document. */ @undoBatch toggleFlex = () => { this.document.gridFlex = !BoolCast(this.document.gridFlex, true); }; /** * Increments the value of numCols on button click */ onIncrementButtonClick = () => { this.clicked = true; this.entered && (this.document.gridNumCols as number)--; undoable(() => { this.document.gridNumCols = this.numCols + 1; }, '')(); this.entered = false; }; /** * Decrements the value of numCols on button click */ onDecrementButtonClick = () => { this.clicked = true; if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; undoable(() => { this.document.gridNumCols = this.numCols - 1; }, '')(); if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; }; /** * Increments the value of numCols on button hover */ incrementValue = () => { this.entered = true; if (!this.clicked && !this.decrementLimitReached) { this.document.gridNumCols = this.numCols + 1; } this.decrementLimitReached = false; this.clicked = false; }; /** * Decrements the value of numCols on button hover */ decrementValue = () => { this.entered = true; if (!this.clicked) { if (this.numCols > 1) { this.document.gridNumCols = this.numCols - 1; } else { this.decrementLimitReached = true; } } this.clicked = false; }; /** * Toggles the value of preventCollision */ toggleCollisions = () => { this.document.gridPreventCollision = !this.document.gridPreventCollision; }; /** * Changes the value of the compactType */ changeCompactType = (e: React.ChangeEvent) => { // need to change startCompaction so that this operation will be undoable. this.document.gridStartCompaction = e.target.selectedOptions[0].value; }; render() { return (
) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
); } }