diff options
Diffstat (limited to 'src/client/views/collections')
28 files changed, 2695 insertions, 893 deletions
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 42f9bb981..6d70cc0d2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -29,7 +29,7 @@ const _global = (window /* browser */ || global) /* node */ as any; @observer export class CollectionDockingView extends CollectionSubView() { - @observable public static Instance: CollectionDockingView; + @observable public static Instance: CollectionDockingView | undefined; public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { return { type: 'react-component', @@ -103,12 +103,14 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean { - const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => (panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document)); - if (tab) { - const j = tab.header.parent.contentItems.indexOf(tab.contentItem); - if (j !== -1) { - tab.header.parent.contentItems[j].remove(); - return CollectionDockingView.Instance.layoutChanged(); + if (CollectionDockingView.Instance) { + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => (panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document)); + if (tab) { + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + if (j !== -1) { + tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); + } } } @@ -119,19 +121,21 @@ export class CollectionDockingView extends CollectionSubView() { public static OpenFullScreen(doc: Doc) { SelectionManager.DeselectAll(); const instance = CollectionDockingView.Instance; - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') { - return DashboardView.openDashboard(doc); + if (instance) { + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') { + return DashboardView.openDashboard(doc); + } + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))], + }; + const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + instance._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + instance._goldenLayout._$maximiseItem(docconfig); + instance._goldenLayout.emit('stateChanged'); + instance.stateChanged(); } - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))], - }; - const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - instance._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - instance._goldenLayout._$maximiseItem(docconfig); - instance._goldenLayout.emit('stateChanged'); - instance.stateChanged(); return true; } @@ -146,21 +150,23 @@ export class CollectionDockingView extends CollectionSubView() { const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); stack.contentItems[activeContentItemIndex].remove(); - return CollectionDockingView.Instance.layoutChanged(); + return instance.layoutChanged(); } - const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tab => tab.contentItem.config.props.panelName === panelName); + const tab = Array.from(instance.tabMap.keys()).find(tab => tab.contentItem.config.props.panelName === panelName); if (tab) { tab.header.parent.addChild(newConfig, undefined); const j = tab.header.parent.contentItems.indexOf(tab.contentItem); !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); - return CollectionDockingView.Instance.layoutChanged(); + return instance.layoutChanged(); } return CollectionDockingView.AddSplit(document, panelName, stack, panelName); } @undoBatch public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { - return Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); + return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 + ? CollectionDockingView.CloseSplit(doc) + : CollectionDockingView.AddSplit(doc, location, stack, panelName); } // @@ -169,8 +175,8 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { - if (document._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); - + if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); + if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); if (tab) { tab.header.parent.setActiveContentItem(tab.contentItem); @@ -453,13 +459,15 @@ export class CollectionDockingView extends CollectionSubView() { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); } - const dview = CollectionDockingView.Instance.props.Document; - const fieldKey = CollectionDockingView.Instance.props.fieldKey; - Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); - this.tabMap.delete(tab); - tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); - tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); - this.stateChanged(); + if (CollectionDockingView.Instance) { + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); + this.tabMap.delete(tab); + tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + setTimeout(this.stateChanged); + } }; tabCreated = (tab: any) => { this.tabMap.add(tab); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 2c0e44bc3..46e8494ab 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -34,7 +34,6 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum import { DocumentView } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; -import { PresBox } from '../nodes/trails/PresBox'; import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionLinearView } from './collectionLinear'; @@ -317,6 +316,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @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]; } @@ -341,6 +343,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu 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: @@ -386,6 +390,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu return <CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />; case CollectionViewType.Stacking: return <CollectionStackingViewChrome key="collchrome" {...this.props} />; + case CollectionViewType.NoteTaking: + return <CollectionNoteTakingViewChrome key="collchrome" {...this.props} />; case CollectionViewType.Schema: return <CollectionSchemaViewChrome key="collchrome" {...this.props} />; case CollectionViewType.Tree: @@ -580,49 +586,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu } @undoBatch - @action - pinWithView = (targetDoc: Opt<Doc>) => { - if (targetDoc) { - TabDocView.PinDoc(targetDoc); - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; - const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1]; - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { - const scroll = targetDoc._scrollTop; - activeDoc.presPinView = true; - activeDoc.presPinViewScroll = scroll; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } else if (targetDoc.type === DocumentType.VID) { - activeDoc.presPinView = true; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const width = targetDoc._clipWidth; - activeDoc.presPinClipWidth = width; - activeDoc.presPinView = true; - } - } - }; - - @computed - get pinWithViewButton() { - const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: 'auto', width: 19 }} />; - return !this.selectedDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Pin with current view'}</div>} placement="top"> - <button className="antimodeMenu-button" style={{ justifyContent: 'center' }} onClick={() => this.pinWithView(this.selectedDoc)}> - {presPinWithViewIcon} - </button> - </Tooltip> - ); - } - - @undoBatch onAlias = () => { if (this.selectedDoc && this.selectedDocumentView) { // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); @@ -715,7 +678,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu {this.aliasButton} {/* {this.pinButton} */} {this.toggleOverlayButton} - {this.pinWithViewButton} <div className="collectionMenu-divider" key="divider2"></div> {this.subChrome} <div className="collectionMenu-divider" key="divider3"></div> @@ -762,6 +724,19 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } + public static gotoKeyFrame(doc: Doc, newFrame: number) { + if (doc) { + const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); + const currentFrame = Cast(doc._currentFrame, 'number', null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); + } + } + @undoBatch @action nextKeyframe = (): void => { @@ -1003,7 +978,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView <FontAwesomeIcon icon={'caret-left'} size={'lg'} /> </div> </Tooltip> - <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + <Tooltip key="num" title={<div className="dash-tooltip">Frame number</div>} placement="bottom"> <div className="numKeyframe" style={{ color: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? 'white' : 'black', backgroundColor: this.props.docView.ComponentView?.getKeyFrameEditing?.() ? '#5B9FDD' : '#AEDDF8' }} @@ -1146,6 +1121,126 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView } @observer +export class CollectionNoteTakingViewChrome extends React.Component<CollectionViewMenuProps> { + @observable private _currentKey: string = ''; + @observable private suggestions: string[] = []; + + get document() { + return this.props.docView.props.Document; + } + + @computed private get descending() { + return StrCast(this.document._columnsSort) === 'descending'; + } + @computed get pivotField() { + return StrCast(this.document._pivotField); + } + + getKeySuggestions = async (value: string): Promise<string[]> => { + 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('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + return keys.filter(key => key.toLowerCase().indexOf(val) > -1); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + const noviceKeys = Array.from(keys).filter( + key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 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); + } else { + const keys = new Set<string>(); + 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.ChangeEvent, { newValue }: { newValue: string }) => { + this._currentKey = newValue; + }; + + getSuggestionValue = (suggestion: string) => suggestion; + + renderSuggestion = (suggestion: string) => { + return <p>{suggestion}</p>; + }; + + 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 : ( + <div className="collectionStackingViewChrome-cont"> + <div className="collectionStackingViewChrome-pivotField-cont"> + <div className="collectionStackingViewChrome-pivotField-label">GROUP BY:</div> + <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? '180' : '0'}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> + <div className="collectionStackingViewChrome-pivotField"> + <EditableView + GetValue={() => 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'} + /> + </div> + </div> + </div> + ); + } +} + +@observer export class CollectionSchemaViewChrome extends React.Component<CollectionViewMenuProps> { // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; get document() { @@ -1442,13 +1537,5 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu } } ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { - const dataField = doc[Doc.LayoutFieldKey(doc)]; - const childDocs = DocListCast(dataField); - const currentFrame = Cast(doc._currentFrame, 'number', null); - if (currentFrame === undefined) { - doc._currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); - } - CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); - doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); + CollectionFreeFormViewChrome.gotoKeyFrame(doc, newFrame); }); diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss new file mode 100644 index 000000000..08b13fd50 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingView.scss @@ -0,0 +1,504 @@ +@import '../global/globalCssVariables'; + +.collectionNoteTakingView-DocumentButtons { + display: none; + justify-content: space-between; + margin: auto; +} + +.collectionNoteTakingView-addDocumentButton { + display: flex; + overflow: hidden; + margin: auto; + width: 100%; + overflow: ellipses; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + width: 100%; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + + font-size: 75%; + letter-spacing: 2px; + cursor: pointer; + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } +} + +.collectionNoteTakingView { + display: flex; +} + +.collectionNoteTakingViewFieldColumn { + height: 100%; + display: flex; + overflow: hidden; +} +.collectionNoteTakingViewFieldColumn:hover { + .collectionNoteTakingView-DocumentButtons { + display: flex; + } +} + +// TODO:glr Turn this into a seperate class +.documentButtonMenu { + position: relative; + height: fit-content; + border-bottom: $standard-border; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + align-content: center; + padding: 5px 0 5px 0; + + .documentExplanation { + width: 90%; + margin: 5px; + font-size: 11px; + background-color: $light-blue; + color: $medium-blue; + padding: 10px; + border-radius: 10px; + border: solid 2px $medium-blue; + } +} + +.collectionNoteTakingView { + height: 100%; + width: 100%; + position: absolute; + top: 0; + overflow-y: auto; + overflow-x: hidden; + transition: top 0.5s; + + > div { + position: relative; + display: block; + } + + .collectionSchemaView-previewDoc { + height: 100%; + position: absolute; + } + + .collectionNoteTakingView-docView-container { + width: 45%; + margin: 5% 2.5%; + padding-left: 2.5%; + height: auto; + } + + .collectionNoteTakingView-columnStack { + height: 100%; + width: 100%; + display: inline-block; + } + .collectionNoteTakingView-Nodes { + width: 100%; + height: 100%; + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + top: 0; + left: 0; + width: 100%; + position: absolute; + margin: auto; + width: max-content; + height: max-content; + position: relative; + grid-auto-rows: 0px; + } + + .collectionNoteTakingView-description { + font-size: 100%; + margin-bottom: 1vw; + padding: 10px; + height: 2vw; + width: 100%; + font-family: $sans-serif; + background: $dark-gray; + color: $white; + } + + .collectionNoteTakingView-columnDragger { + width: 15; + height: 15; + position: absolute; + margin-left: -5; + } + + .collectionNoteTakingView-sectionDelete { + display: none; + position: absolute; + right: 0; + width: max-content; + height: max-content; + top: 10; + padding: 2; + } + + // Documents in NoteTaking view + .collectionNoteTakingView-columnDoc { + display: flex; + // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change + position: relative; + + &:hover { + .hoverButtons { + opacity: 1; + } + } + + .hoverButtons { + display: flex; + opacity: 0; + position: absolute; + height: 100%; + left: -35px; + justify-content: center; + align-items: center; + padding: 0px 10px; + + .buttonWrapper { + padding: 3px; + border-radius: 3px; + + &:hover { + background: rgba(0, 0, 0, 0.26); + } + } + } + } + + .collectionNoteTakingView-collapseBar { + margin-left: 2px; + margin-right: 2px; + margin-top: 2px; + background: $medium-gray; + height: 5px; + + &.active { + margin-left: 0; + margin-right: 0; + background: red; + } + } + + .collectionNoteTakingView-miniHeader { + width: 100%; + + .editableView-container-editing-oneLine { + min-height: 20px; + display: flex; + align-items: center; + flex-direction: row; + } + + span::before, + span::after { + content: ''; + width: 50%; + border-top: dashed gray 1px; + position: relative; + display: inline-block; + } + + span::before { + margin-right: 10px; + } + + span::after { + margin-left: 10px; + } + + span { + position: relative; + text-align: center; + white-space: nowrap; + overflow: visible; + width: 100%; + display: flex; + color: gray; + align-items: center; + } + } + + .collectionNoteTakingView-sectionHeader { + text-align: center; + margin: auto; + margin-bottom: 10px; + background: $medium-gray; + // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong + + .editableView-input { + color: $dark-gray; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .collectionNoteTakingView-sectionHeader-subCont { + outline: none; + border: 0px; + width: 100%; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + position: relative; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + color: $dark-gray; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + padding: 12px 10px 11px 10px; + border: 0px; + color: grey; + text-align: center; + letter-spacing: 2px; + outline-color: black; + } + } + + .collectionNoteTakingView-sectionColor { + position: absolute; + left: 0; + top: 0; + height: 100%; + display: none; + + [class*='css'] { + max-width: 102px; + } + + .collectionNoteTakingView-sectionColorButton { + height: 30px; + display: inherit; + } + + .collectionNoteTakingView-colorPicker { + width: 78px; + z-index: 10; + position: relative; + background: white; + + .colorOptions { + display: flex; + flex-wrap: wrap; + } + + .colorPicker { + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 10px; + margin: 3px; + + &.active { + border: 2px solid white; + box-shadow: 0 0 0 2px lightgray; + } + } + } + } + + .collectionNoteTakingView-sectionOptions { + position: absolute; + right: 0; + top: 0; + height: 100%; + display: none; + + [class*='css'] { + max-width: 102px; + } + + .collectionNoteTakingView-sectionOptionButton { + height: 30px; + } + + .collectionNoteTakingView-optionPicker { + .optionOptions { + display: inline; + } + + .optionPicker { + cursor: pointer; + height: 20px; + border-radius: 10px; + margin: 3px; + width: max-content; + + &.active { + color: red; + } + } + } + } + } + + .collectionNoteTakingView-sectionHeader:hover { + .collectionNoteTakingView-sectionColor { + display: unset; + } + + .collectionNoteTakingView-sectionOptions { + display: unset; + } + + .collectionNoteTakingView-sectionDelete { + display: unset; + } + } + + .collectionNoteTakingView-addDocumentButton, + .collectionNoteTakingView-addGroupButton { + display: flex; + overflow: hidden; + margin: auto; + width: 90%; + overflow: ellipses; + + .editableView-container-editing-oneLine, + .editableView-container-editing { + color: grey; + padding: 10px; + width: 100%; + } + + .editableView-input:hover, + .editableView-container-editing:hover, + .editableView-container-editing-oneLine:hover { + cursor: text; + } + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + } + + .collectionNoteTakingView-addDocumentButton { + font-size: 75%; + letter-spacing: 2px; + cursor: pointer; + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + } + + .collectionNoteTakingView-addGroupButton { + background: rgb(238, 238, 238); + font-size: 75%; + text-align: center; + letter-spacing: 2px; + height: fit-content; + } + + .rc-switch { + position: absolute; + display: inline-block; + bottom: 4px; + right: 4px; + width: 70px; + height: 30px; + border-radius: 40px 40px; + background-color: lightslategrey; + } + + .rc-switch:after { + position: absolute; + width: 22px; + height: 22px; + left: 3px; + top: 4px; + border-radius: 50% 50%; + background-color: #fff; + content: ' '; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); + -webkit-transform: scale(1); + transform: scale(1); + transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-duration: 0.3s; + animation-duration: 0.3s; + } + + .rc-switch-checked:after { + left: 44px; + } + + .rc-switch-inner { + color: #fff; + font-size: 12px; + position: absolute; + left: 28px; + top: 8px; + } + + .rc-switch-checked .rc-switch-inner { + left: 8px; + } +} + +@media only screen and (max-device-width: 480px) { + .collectionNoteTakingView .collectionNoteTakingView-columnDragger, + .collectionNoteTakingView-columnDragger { + width: 0.1; + height: 0.1; + opacity: 0; + font-size: 0; + } +} diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx new file mode 100644 index 000000000..92c0bc341 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -0,0 +1,707 @@ +import React = require('react'); +import { CursorProperty } from 'csstype'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Copy, Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager, dropActionType } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { LightboxView } from '../LightboxView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { FieldViewProps } from '../nodes/FieldView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { StyleProp } from '../StyleProvider'; +import './CollectionNoteTakingView.scss'; +import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn'; +import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider'; +import { CollectionSubView } from './CollectionSubView'; +const _global = (window /* browser */ || global) /* node */ as any; + +/** + * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1) + * add and remove columns (2) change column sizes and (3) move documents within and between columns. This + * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is + * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in + * the rest of Dash, so it may be worthwhile to transition the headers to simple documents. + */ +@observer +export class CollectionNoteTakingView extends CollectionSubView() { + _disposers: { [key: string]: IReactionDisposer } = {}; + _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef<HTMLDivElement>(); + notetakingCategoryField = 'NotetakingCategory'; + public DividerWidth = 16; + @observable docsDraggedRowCol: number[] = []; + @observable _cursor: CursorProperty = 'grab'; + @observable _scroll = 0; + @computed get chromeHidden() { + return BoolCast(this.layoutDoc.chromeHidden); + } + // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns + @computed get columnHeaders() { + const columnHeaders = 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 === undefined || columnHeaders.length === 0) { + setTimeout(() => { + const columnHeaders = 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 === undefined || columnHeaders.length === 0) { + if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); + else this.dataDoc.columnHeaders = new List<SchemaHeaderField>(); + } + }); + } + return columnHeaders ?? ([] as SchemaHeaderField[]); + } + @computed get headerMargin() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); + } + @computed get xMargin() { + return NumCast(this.layoutDoc._xMargin, 5); + } + @computed get yMargin() { + return NumCast(this.layoutDoc._yMargin, 5); + } + @computed get gridGap() { + return NumCast(this.layoutDoc._gridGap, 10); + } + // numGroupColumns returns the number of columns + @computed get numGroupColumns() { + return this.columnHeaders.length; + } + // PanelWidth returns the size of the total available space the view occupies + @computed get PanelWidth() { + return this.props.PanelWidth(); + } + // maxColWidth returns the maximum column width, which is slightly less than the total available space. + @computed get maxColWidth() { + return this.props.PanelWidth(); + } + // availableWidth is the total amount of non-divider width. Since widths are stored relatively, + // we use availableWidth to convert from a percentage to a pixel count. + @computed get availableWidth() { + const numDividers = this.numGroupColumns - 1; + return this.maxColWidth - numDividers * this.DividerWidth; + } + + // children is passed as a prop to the NoteTakingField, which uses this function + // to render the docs you see within an individual column. + children = (docs: Doc[]) => { + TraceMobx(); + return docs.map((d, i) => { + const height = () => this.getDocHeight(d); + const width = () => this.getDocWidth(d); + const style = { width: width(), marginTop: this.gridGap, height: height() }; + return ( + <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}> + {this.getDisplayDoc(d, width)} + </div> + ); + }); + }; + + // Sections is one of the more important functions in this file, rendering the the documents + // for the UI. It properly renders documents being dragged between columns. + // [CAVEATS] (1) keep track of the offsetting + // (2) documentView gets unmounted as you remove it from the list + @computed get Sections() { + TraceMobx(); + const columnHeaders = this.columnHeaders; + // filter out the currently dragged docs from the child docs, since we will insert them later + const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); + const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + const rowCol = this.docsDraggedRowCol; + // this will sort the docs into the correct columns (minus the ones you're currently dragging) + docs.map(d => { + const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; + // look for if header exists already + const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); + if (existingHeader) { + sections.get(existingHeader)!.push(d); + } + }); + // now we add back in the docs that we're dragging + if (rowCol.length && columnHeaders.length > rowCol[1]) { + const offset = 0; + sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); + } + return sections; + } + + removeDocDragHighlight = () => { + setTimeout( + action(() => (this.docsDraggedRowCol.length = 0)), + 100 + ); + }; + + componentDidMount() { + super.componentDidMount?.(); + document.addEventListener('pointerup', this.removeDocDragHighlight, true); + this._disposers.autoHeight = reaction( + () => this.layoutDoc._autoHeight, + autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) + ); + } + + componentWillUnmount() { + document.removeEventListener('pointerup', this.removeDocDragHighlight, true); + super.componentWillUnmount(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => { + return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; + }; + + createRef = (ele: HTMLDivElement | null) => { + this._masonryGridRef = ele; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? + }; + + @computed get onChildClickHandler() { + return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); + } + + @computed get onChildDoubleClickHandler() { + return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); + } + + addDocTab = (doc: Doc, where: string) => { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + }; + + scrollToBottom = () => { + smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); + }; + + // let's dive in and get the actual document we want to drag/move around + focusDocument = (doc: Doc, options?: DocFocusOptions) => { + Doc.BrushDoc(doc); + let focusSpeed = 0; + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + if (found) { + const top = found.getBoundingClientRect().top; + const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); + if (Math.floor(localTop[1]) !== 0) { + smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + } + } + const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing); + this.props.focus(this.rootDoc, { + willZoom: options?.willZoom, + scale: options?.scale, + afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), + }); + }; + + styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { + if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) { + return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`; + } + if (property === StyleProp.Opacity && doc) { + if (this.props.childOpacity) { + return this.props.childOpacity(); + } + } + return this.props.styleProvider?.(doc, props, property); + }; + + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + + // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) + getDisplayDoc(doc: Doc, width: () => number) { + const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; + const height = () => this.getDocHeight(doc); + let dref: Opt<DocumentView>; + const noteTakingDocTransform = () => this.getDocTransform(doc, dref); + return ( + <DocumentView + ref={r => (dref = r || undefined)} + Document={doc} + DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + styleProvider={this.styleProvider} + docViewPath={this.props.docViewPath} + fitWidth={this.props.childFitWidth} + isContentActive={emptyFunction} + onKey={this.onKeyDown} + //TODO: change this from a prop to a parameter passed into a function + dontHideOnDrag={true} + isDocumentActive={this.isContentActive} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} + dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} + dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} + rootSelected={this.rootSelected} + showTitle={this.props.childShowTitle} + dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={noteTakingDocTransform} + focus={this.focusDocument} + docFilters={this.childDocFilters} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideTitle={this.props.childHideTitle?.()} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + bringToFront={returnFalse} + scriptContext={this.props.scriptContext} + pinToPres={this.props.pinToPres} + /> + ); + } + + // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns + getDocTransform(doc: Doc, dref?: DocumentView) { + const y = this._scroll; // required for document decorations to update when the text box container is scrolled + const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); + // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off + return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); + } + + // how to get the width of a document. Currently returns the width of the column (minus margins) + // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) + getDocWidth(d: Doc) { + const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); + const existingHeader = this.columnHeaders.find(sh => sh.heading === heading); + const existingWidth = existingHeader?.width ? existingHeader.width : 0; + const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; + const width = d.fitWidth ? maxWidth : d[WidthSym](); + return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); + } + + // how to get the height of a document. Nothing special here. + getDocHeight(d?: Doc) { + if (!d || d.hidden) return 0; + const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc; + const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0); + if (nw && nh) { + const docWid = this.getDocWidth(d); + return Math.min(maxHeight, (docWid * nh) / nw); + } + const childHeight = NumCast(childLayoutDoc._height); + const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; + return Math.min(childHeight, maxHeight, panelHeight); + } + + // resizeColumns is called whenever a user adds or removes a column. When removing, + // this function renormalizes the column widths to fill the newly available space + // in the panel. When adding, this function renormalizes the existing columns to take up + // (n - 1)/n space, since the new column will be allocated 1/n of the total space. + // Column widths are relative (portion of available space) and stored in the 'width' + // field of SchemaHeaderFields. + // + // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375]. + // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33] + @action + resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => { + const n = this.columnHeaders.length; + if (n == 1) { + this.columnHeaders[0].setWidth(1); + return true; + } + const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth); + this.columnHeaders.forEach((h, i) => { + if (!(isAdd && i == colIndex)) { + h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor); + } + }); + return true; + }; + + // onPointerMove is used to preview where a document will drop in a column once a drag is complete. + @action + onPointerMove = (force: boolean, ex: number, ey: number) => { + if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) { + // get the current docs for the column based on the mouse's x coordinate + const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap; + const colDocs = this.getDocsFromXCoord(xCoord); + // get the index for where you need to insert the doc you are currently dragging + const clientY = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[1]; + let dropInd = -1; + let pos0 = (this.refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2; + colDocs.forEach((doc, i) => { + let pos1 = this.getDocHeight(doc) + 2 * this.gridGap; + pos1 += pos0; + // updating drop position based on y coordinates + const yCoordInBetween = clientY > pos0 && clientY < pos1; + if (yCoordInBetween || (clientY < pos0 && i === 0)) { + dropInd = i; + } else if (i === colDocs.length - 1 && dropInd === -1) { + dropInd = !colDocs.includes(DragManager.docsBeingDragged.lastElement()) ? i + 1 : i; + } + pos0 = pos1; + }); + // we alter the pivot fields of the docs in case they are moved to a new column. + const colIndex = this.getColumnFromXCoord(xCoord); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader)); + // used to notify sections to re-render + this.docsDraggedRowCol.length = 0; + const columnFromCoord = this.getColumnFromXCoord(xCoord); + columnFromCoord !== undefined && this.docsDraggedRowCol.push(dropInd, columnFromCoord); + } + }; + + // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate). + // This function is used to know which document a column SHOULD be in while it is being dragged. + getColumnFromXCoord = (xCoord: number): number | undefined => { + let colIndex: number | undefined = undefined; + const numColumns = this.columnHeaders.length; + const coords = []; + let colStartXCoord = 0; + for (let i = 0; i < numColumns; i++) { + coords.push(colStartXCoord); + colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.DividerWidth; + } + coords.push(this.PanelWidth); + for (let i = 0; i < numColumns; i++) { + if (xCoord > coords[i] && xCoord < coords[i + 1]) { + colIndex = i; + break; + } + } + return colIndex; + }; + + // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided. + getDocsFromXCoord = (xCoord: number): Doc[] => { + const docsMatchingHeader: Doc[] = []; + const colIndex = this.getColumnFromXCoord(xCoord); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + this.childDocs?.map(d => { + if (d instanceof Promise) return; + const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; + if (sectionValue.toString() == colHeader) { + docsMatchingHeader.push(d); + } + }); + return docsMatchingHeader; + }; + + @undoBatch + @action + onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { + const docView = fieldProps.DocumentView?.(); + if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) { + e.stopPropagation?.(); + const newDoc = Doc.MakeCopy(docView.rootDoc, true); + Doc.GetProto(newDoc).text = undefined; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + return this.addDocument?.(newDoc); + } + }; + + // onInternalDrop is used when dragging and dropping a document within the view, such as dragging + // a document to a new column or changing its order within the column. + @undoBatch + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + if (super.onInternalDrop(e, de)) { + // filter out the currently dragged docs from the child docs, since we will insert them later + const rowCol = this.docsDraggedRowCol; + const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). + const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; + const docs = this.childDocList; + if (docs && newDocs.length) { + // remove the dragged documents from the childDocList + newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1)); + // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly + if (rowCol[0] <= 0) { + docs.splice(0, 0, ...newDocs); + } else { + const colDocs = this.getDocsFromXCoord(this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]); + const previousDoc = colDocs[rowCol[0] - 1]; + const previousDocIndex = docs.indexOf(previousDoc); + docs.splice(previousDocIndex + 1, 0, ...newDocs); + } + } + } + } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); + this.props.addDocument?.(source); + de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed + e.stopPropagation(); + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return false; + }; + + @undoBatch + internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { + const dropCreator = annoDragData.dropDocCreator; + annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { + const dropDoc = dropCreator(annotationOn); + return dropDoc || this.rootDoc; + }; + return true; + } + + // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView + // to another tab/view/collection + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + const targInd = this.docsDraggedRowCol?.[0] || 0; + const colInd = this.docsDraggedRowCol?.[1] || 0; + super.onExternalDrop( + e, + {}, + undoBatch( + action(docus => { + this.onPointerMove(true, e.clientX, e.clientY); + docus?.map((doc: Doc) => this.addDocument(doc)); + const newDoc = this.childDocs.lastElement(); + const colHeader = colInd === undefined ? 'unset' : StrCast(this.columnHeaders[colInd].heading); + newDoc[this.notetakingCategoryField] = colHeader; + const docs = this.childDocList; + if (docs && targInd !== -1) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + this.removeDocDragHighlight(); + }) + ) + ); + }; + + headings = () => Array.from(this.Sections); + + refList: any[] = []; + + editableViewProps = () => ({ + GetValue: () => '', + SetValue: this.addGroup, + contents: '+ New Column', + }); + + // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) + sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + const type = 'number'; + return ( + <CollectionNoteTakingViewColumn + unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={ref => { + if (ref) { + this.refList.push(ref); + this.observer = new _global.ResizeObserver( + action((entries: any) => { + if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); + if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) { + this.props.setHeight?.(height); + } + } + }) + ); + this.observer.observe(ref); + } + }} + addDocument={this.addDocument} + chromeHidden={this.chromeHidden} + columnHeaders={this.columnHeaders} + Document={this.props.Document} + DataDoc={this.props.DataDoc} + resizeColumns={this.resizeColumns} + renderChildren={this.children} + numGroupColumns={this.numGroupColumns} + gridGap={this.gridGap} + pivotField={this.notetakingCategoryField} + dividerWidth={this.DividerWidth} + maxColWidth={this.maxColWidth} + availableWidth={this.availableWidth} + PanelWidth={this.PanelWidth} + key={heading?.heading ?? 'unset'} + headings={this.headings} + heading={heading?.heading ?? 'unset'} + headingObject={heading} + docList={docList} + yMargin={this.yMargin} + type={type} + createDropTarget={this.createDashEventsTarget} + screenToLocalTransform={this.props.ScreenToLocalTransform} + editableViewProps={this.editableViewProps} + /> + ); + }; + + // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of + // columnHeaders and resizing the existing columns to make room for our new one. + @undoBatch + @action + addGroup = (value: string) => { + if (this.columnHeaders) { + for (const header of this.columnHeaders) { + if (header.heading == value) { + alert('You cannot use an existing column name. Please try a new column name'); + return value; + } + } + } + const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); + const newColWidth = 1 / (this.numGroupColumns + 1); + value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1); + this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]())); + return true; + }; + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + const subItems: ContextMenuProps[] = []; + subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); + subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' }); + subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' }); + ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); + } + }; + + // setColumnStartXCoords is used to update column widths when using the drag handlers between columns + @action + setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { + const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0]; + const leftHeader = this.columnHeaders[colIndex]; + const rightHeader = this.columnHeaders[colIndex + 1]; + leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); + rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); + }; + + // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view + // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that + // allows the user to adjust the column widths. + @computed get renderedSections() { + TraceMobx(); + const entries = Array.from(this.Sections.entries()); + const sections = entries; + const eles: JSX.Element[] = []; + for (let i = 0; i < sections.length; i++) { + const col = this.sectionNoteTaking(sections[i][0], sections[i][1]); + eles.push(col); + if (i < sections.length - 1) { + eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); + } + } + return eles; + } + + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + if (menuDoc) { + const width = NumCast(menuDoc._width, 30); + const height = NumCast(menuDoc._height, 30); + return ( + <div className="buttonMenu-docBtn" style={{ width, height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={this.props.renderDepth} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + } + } + + @computed get nativeWidth() { + return Doc.NativeWidth(this.layoutDoc); + } + @computed get nativeHeight() { + return Doc.NativeHeight(this.layoutDoc); + } + + @computed get scaling() { + return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; + } + + @computed get backgroundEvents() { + return SnappingManager.GetIsDragging(); + } + + observer: any; + + render() { + TraceMobx(); + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = StrCast(this.rootDoc.explainer); + return ( + <> + {buttonMenu || noviceExplainer ? ( + <div className="documentButtonMenu" key="buttons"> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null} + </div> + ) : null} + <div + className="collectionNoteTakingView" + ref={this.createRef} + key="notes" + style={{ + overflowY: this.props.isContentActive() ? 'auto' : 'hidden', + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? 'all' : undefined, + }} + onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} + onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} + onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} + onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> + {this.renderedSections} + </div> + </> + ); + } +} diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx new file mode 100644 index 000000000..84d1c0205 --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -0,0 +1,310 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Copy, Id } from '../../../fields/FieldSymbols'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { Cast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { returnEmptyString } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import './CollectionNoteTakingView.scss'; + +interface CSVFieldColumnProps { + Document: Doc; + DataDoc: Opt<Doc>; + docList: Doc[]; + heading: string; + pivotField: string; + chromeHidden?: boolean; + columnHeaders: SchemaHeaderField[] | undefined; + headingObject: SchemaHeaderField | undefined; + yMargin: number; + numGroupColumns: number; + gridGap: number; + type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; + headings: () => object[]; + renderChildren: (docs: Doc[]) => JSX.Element[]; + addDocument: (doc: Doc | Doc[]) => boolean; + createDropTarget: (ele: HTMLDivElement) => void; + screenToLocalTransform: () => Transform; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; + editableViewProps: () => any; + resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean; + PanelWidth: number; + maxColWidth: number; + dividerWidth: number; + availableWidth: number; +} + +/** + * CollectionNoteTakingViewColumn represents an individual column rendered in CollectionNoteTakingView. The + * majority of functions here are for rendering styles. + */ +@observer +export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> { + @observable private _background = 'inherit'; + + // columnWidth returns the width of a column in absolute pixels + @computed get columnWidth() { + if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length === 1) return '100%'; + const i = this.props.columnHeaders.indexOf(this.props.headingObject); + return this.props.columnHeaders[i].width * 100 + '%'; + } + + private dropDisposer?: DragManager.DragDropDisposer; + private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + + public static ColumnMargin = 10; + @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; + @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; + _ele: HTMLElement | null = null; + + createColumnDropRef = (ele: HTMLDivElement | null) => { + this.dropDisposer?.(); + if (ele) { + this._ele = ele; + this.props.observeHeight(ele); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); + } + }; + + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } + + @undoBatch + columnDrop = action((e: Event, de: DragManager.DropEvent) => { + const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; + drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + }); + + getValue = (value: string): any => { + const parsed = parseInt(value); + if (!isNaN(parsed)) return parsed; + if (value.toLowerCase().indexOf('true') > -1) return true; + if (value.toLowerCase().indexOf('false') > -1) return false; + return value; + }; + + @action + headingChanged = (value: string, shiftDown?: boolean) => { + const castedValue = this.getValue(value); + if (castedValue) { + if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { + return false; + } + this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); + if (this.props.headingObject) { + this.props.headingObject.setHeading(castedValue.toString()); + this._heading = this.props.headingObject.heading; + } + return true; + } + return false; + }; + + @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); + @action pointerLeave = () => (this._background = 'inherit'); + textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + + // addNewTextDoc is called when a user starts typing in a column to create a new node + @action + addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { + if (!value && !forceEmptyNote) return false; + const key = this.props.pivotField; + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); + const colValue = this.getValue(this.props.heading); + newDoc[key] = colValue; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + return this.props.addDocument?.(newDoc) || false; + }; + + // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. + // If the user deletes the first column, the documents get moved to the second column. Otherwise, + // all docs are added to the column directly to the left. + @undoBatch + @action + deleteColumn = () => { + const acolumnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); + if (acolumnHeaders && this.props.headingObject) { + const index = acolumnHeaders.indexOf(this.props.headingObject); + const columnHeaders = acolumnHeaders; // new List<SchemaHeaderField>(acolumnHeaders.map(header => header[Copy]())); // needed for undo to work properly. otherwise we end up changing field values in the undo stack since they are shared by reference + const newColIndex = index > 0 ? index - 1 : 1; + const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined; + const newHeading = newColHeader ? newColHeader.heading : 'unset'; + this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading)); + const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0; + columnHeaders.splice(index, 1); + //Doc.GetProto(this.props.Document).columnHeaders = columnHeaders; + this.props.resizeColumns(false, colWidth, index); + } + }; + + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + const dataDoc = this.props.DataDoc || this.props.Document; + const pivotValue = this.getValue(this.props.heading); + + DocUtils.addDocumentCreatorMenuItems( + doc => { + const key = this.props.pivotField; + doc[key] = this.getValue(this.props.heading); + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.addDocument?.(doc); + }, + this.props.addDocument, + x, + y, + true, + this.props.pivotField, + pivotValue + ); + + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + return this.props.addDocument?.(created); + } + }, + icon: 'compress-arrows-alt', + }) + ); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + } + return this.props.addDocument?.(created) || false; + } + }, + icon: 'compress-arrows-alt', + }) + ); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); + ContextMenu.Instance.setDefaultItem('::', (name: string): void => { + Doc.GetProto(this.props.Document)[name] = ''; + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + this.props.addDocument?.(created); + } + }); + ContextMenu.Instance.displayMenu(x, y, undefined, true); + }; + + @computed get innards() { + TraceMobx(); + const key = this.props.pivotField; + const heading = this._heading; + const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin; + const evContents = heading ? heading : '25'; + const headingView = this.props.headingObject ? ( + <div + key={heading} + className="collectionNoteTakingView-sectionHeader" + ref={this._headerRef} + style={{ + marginTop: 2 * this.props.yMargin, + width: 'calc(100% - 5px)', + }}> + <div + className="collectionNoteTakingView-sectionHeader-subCont" + title={evContents === `No Value` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''} + style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}> + <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} /> + </div> + {(this.props.columnHeaders?.length ?? 0) > 1 && ( + <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button> + )} + </div> + ) : null; + const templatecols = this.columnWidth; + const type = this.props.Document.type; + return ( + <> + {headingView} + <div className="collectionNoteTakingView-columnStack"> + <div + key={`${heading}-stack`} + className={`collectionNoteTakingView-Nodes`} + style={{ + padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`, + gridGap: this.props.gridGap, + gridTemplateColumns: templatecols, + }}> + {this.props.renderChildren(this.props.docList)} + </div> + + {!this.props.chromeHidden && type !== DocumentType.PRES ? ( + <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}> + <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> + </div> + <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton"> + <EditableView {...this.props.editableViewProps()} /> + </div> + </div> + ) : null} + </div> + </> + ); + } + + render() { + TraceMobx(); + const heading = this._heading; + return ( + <div + className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} + key={heading} + style={{ + width: this.columnWidth, + background: this._background, + }} + ref={this.createColumnDropRef} + onPointerEnter={this.pointerEntered} + onPointerLeave={this.pointerLeave}> + {this.innards} + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx new file mode 100644 index 000000000..a1309b11f --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx @@ -0,0 +1,65 @@ +import { action, observable } from 'mobx'; +import * as React from 'react'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { UndoManager } from '../../util/UndoManager'; + +interface DividerProps { + index: number; + xMargin: number; + setColumnStartXCoords: (movementX: number, colIndex: number) => void; +} + +/** + * CollectionNoteTakingViewDivider are dividers between CollectionNoteTakingViewColumns, + * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers + * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns. + */ +export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { + const batch = UndoManager.StartBatch('resizing'); + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + this.props.setColumnStartXCoords(delta[0], this.props.index); + return false; + }, + action(() => { + this.isResizingActive = false; + this.isHoverActive = false; + batch.end(); + }), + emptyFunction + ); + this.isResizingActive = true; + }; + + render() { + return ( + <div + className="columnResizer" + style={{ + display: 'flex', + alignItems: 'center', + cursor: 'col-resize', + }} + onPointerEnter={action(() => (this.isHoverActive = true))} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}> + <div + className="columnResizer-handler" + onPointerDown={e => this.registerResizing(e)} + style={{ + height: '95%', + width: 12, + borderRight: '4px solid #282828', + borderLeft: '4px solid #282828', + }} + /> + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index bb98e1c99..5a107d2ca 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,7 +1,8 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; -.timeline-container { +.collectionStackedTimeline-timelineContainer { height: 100%; + position: absolute; overflow-x: auto; overflow-y: hidden; border: none; @@ -9,13 +10,14 @@ border-width: 0 2px 0 2px; &:hover { + cursor: default; .collectionStackedTimeline-hover { display: block; } } } -.timeline-container:hover + .timeline-hoverUI { +.collectionStackedTimeline-timelineContainer:hover + .timeline-hoverUI { display: flex; justify-content: center; } @@ -72,7 +74,8 @@ border-width: 1px; } - .collectionStackedTimeline-current, .collectionStackedTimeline-hover { + .collectionStackedTimeline-current, + .collectionStackedTimeline-hover { width: 1px; height: 100%; position: absolute; @@ -107,14 +110,15 @@ height: 100%; width: 10px; pointer-events: all; - cursor: ew-resize; z-index: 100; } .collectionStackedTimeline-resizer { right: 0; + cursor: e-resize; } .collectionStackedTimeline-left-resizer { left: 0; + cursor: w-resize; } } @@ -141,5 +145,6 @@ transform: translate(0, -100%); font-weight: bold; + pointer-events: none; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index dcf3f7c51..b29abf083 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,7 +2,7 @@ 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 } from '../../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -244,6 +244,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack if (!wasSelecting) { this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); wasSelecting = true; + this._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize'); } this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); return false; @@ -260,6 +261,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } (!isClick || !wasSelecting) && (this._markerEnd = undefined); + this._timelineWrapper && (this._timelineWrapper.style.cursor = ''); }), (e, doubleTap) => { if (e.button !== 2) { @@ -289,10 +291,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._hoverTime = this.toTimeline(clientX - rect.x, rect.width); if (this.dataDoc.thumbnails) { const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails); - const thumbnails = Cast(this.dataDoc.thumbnails, listSpec('string'), []); - const imgField = thumbnails && thumbnails.length > 0 ? new ImageField(thumbnails[nearest]) : new ImageField(''); - const src = imgField && imgField.url.href ? imgField.url.href.replace('.png', '_s.png') : ''; - this._thumbnail = src ? src : undefined; + const thumbnails = StrListCast(this.dataDoc.thumbnails); + const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined; + this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined; } } }; @@ -519,7 +520,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack isAnnotationOverlay={true} isDocumentActive={returnFalse} select={emptyFunction} - scaling={returnOne} + NativeDimScaling={returnOne} xMargin={25} yMargin={10} ScreenToLocalTransform={this.dictationScreenToLocalTransform} @@ -561,8 +562,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack return ( <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> <div - className="timeline-container" - style={{ width: this.props.PanelWidth() }} + className="collectionStackedTimeline-timelineContainer" + style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }} onWheel={e => e.stopPropagation()} onScroll={this.setScroll} onMouseMove={e => this.isContentActive() && this.onHover(e)} @@ -669,7 +670,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack )} </div> </div> - <div className="timeline-hoverUI" style={{ left: `calc(${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%` }}> + <div className="timeline-hoverUI" style={{ left: ((this._hoverTime - this.clipStart) / this.clipDuration) * this.timelineContentWidth - this._scroll }}> <div className="hoverTime">{formatTime(this._hoverTime - this.clipStart)}</div> {this._thumbnail && <img className="videoBox-thumbnail" src={this._thumbnail} />} </div> diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 73572299a..7385f933b 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .collectionMasonryView { display: inline; @@ -33,7 +33,7 @@ color: $medium-blue; padding: 10px; border-radius: 5px; - border: solid .5px $medium-blue; + border: solid 0.5px $medium-blue; } } @@ -46,9 +46,9 @@ overflow-y: auto; overflow-x: hidden; flex-wrap: wrap; - transition: top .5s; + transition: top 0.5s; - >div { + > div { position: relative; display: block; } @@ -130,9 +130,11 @@ margin-left: -5; } + // Documents in stacking view .collectionStackingView-columnDoc { - display: inline-block; - margin: auto; + display: flex; + // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change + position: relative; } .collectionStackingView-masonryDoc { @@ -173,7 +175,7 @@ span::before, span::after { - content: ""; + content: ''; width: 50%; border-top: dashed gray 1px; position: relative; @@ -203,6 +205,7 @@ .collectionStackingView-sectionHeader { text-align: center; margin: auto; + margin-bottom: 10px; background: $medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong @@ -213,7 +216,7 @@ .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { - cursor: text + cursor: text; } .collectionStackingView-sectionHeader-subCont { @@ -259,7 +262,7 @@ height: 100%; display: none; - [class*="css"] { + [class*='css'] { max-width: 102px; } @@ -301,7 +304,7 @@ height: 100%; display: none; - [class*="css"] { + [class*='css'] { max-width: 102px; } @@ -310,7 +313,6 @@ } .collectionStackingView-optionPicker { - .optionOptions { display: inline; } @@ -370,7 +372,7 @@ .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { - cursor: text + cursor: text; } .editableView-input { @@ -385,6 +387,7 @@ .collectionStackingView-addDocumentButton { font-size: 75%; letter-spacing: 2px; + cursor: pointer; .editableView-input { outline-color: black; @@ -422,7 +425,7 @@ top: 4px; border-radius: 50% 50%; background-color: #fff; - content: " "; + content: ' '; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); -webkit-transform: scale(1); @@ -452,7 +455,6 @@ } @media only screen and (max-device-width: 480px) { - .collectionStackingView .collectionStackingView-columnDragger, .collectionMasonryView .collectionStackingView-columnDragger { width: 0.1; @@ -460,4 +462,4 @@ opacity: 0; font-size: 0; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6850fb23a..cc006c734 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -34,6 +34,7 @@ const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { chromeHidden?: boolean; + // view type is stacking viewType?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; @@ -42,50 +43,69 @@ export type collectionStackingViewProps = { @observer export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() { _masonryGridRef: HTMLDivElement | null = null; + // used in a column dragger, likely due for the masonry grid view. We want to use this _draggerRef = React.createRef<HTMLDivElement>(); + // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view _pivotFieldDisposer?: IReactionDisposer; + // Seems like we cause reaction in MobX get rid of our height once we exit this view _autoHeightDisposer?: IReactionDisposer; + // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; + // Doesn't look like this field is being used anywhere. Obsolete? _columnStart: number = 0; + // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map<string, number>(); + // Assuming that this is the current css cursor style @observable _cursor: CursorProperty = 'grab'; + // gets reset whenever we scroll. Not sure what it is @observable _scroll = 0; // used to force the document decoration to update when scrolling + // does this mean whether the browser is hidden? Or is chrome something else entirely? @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } + // it looks like this gets the column headers that Mehek was showing just now @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); } + // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } + // filteredChildren is what you want to work with. It's the list of things that you're currently displaying @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } + // how much margin we give the header @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { - return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth())); + return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth())); } @computed get yMargin() { - return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); - } // 2 * this.gridGap)); } + return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth())); + } + @computed get gridGap() { - return NumCast(this.layoutDoc._gridGap, 10); + return NumCast(this.layoutDoc._gridGap, 5); } + // are we stacking or masonry? @computed get isStackingView() { return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; } + // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } + // reveals a button to add a group in masonry view @computed get showAddAGroup() { return this.pivotField && !this.chromeHidden; } + // columnWidth handles the margin on the left and right side of the documents @computed get columnWidth() { return Math.min(this.props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); } + @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } @@ -94,18 +114,26 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection super(props); if (this.columnHeaders === undefined) { + // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out? + // here we're making an empty list of column headers (again, what Mehek showed us) this.layoutDoc._columnHeaders = new List<SchemaHeaderField>(); } } + // TODO: plj - these are the children children = (docs: Doc[]) => { + //TODO: can somebody explain me to what exactly TraceMobX is? TraceMobx(); + // appears that we are going to reset the _docXfs. TODO: what is Xfs? this._docXfs.length = 0; return docs.map((d, i) => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); + // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + // just getting the style const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> {this.getDisplayDoc(d, width)} @@ -118,7 +146,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this._heightMap.set(key, sectionHeight); }; + // is sections that all collections inherit? I think this is how we show the masonry/columns + //TODO: this seems important get Sections() { + // appears that pivot field IS actually for sorting if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); if (this.columnHeaders === undefined) { @@ -146,6 +177,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }); // remove all empty columns if hideHeadings is set + // we will want to have something like this, so that we can hide columns and add them back in if (this.layoutDoc._columnsHideIfEmpty) { Array.from(fields.keys()) .filter(key => !fields.get(key)!.length) @@ -218,6 +250,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); }; + // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options?: DocFocusOptions) => { Doc.BrushDoc(doc); @@ -227,7 +260,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } } const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing; @@ -268,8 +301,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => (this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined); + isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); + + isChildContentActive = () => + this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined; + // this is what renders the document that you see on the screen + // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); @@ -277,6 +314,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection let dref: Opt<DocumentView>; const stackedDocTransform = () => this.getDocTransform(doc, dref); this._docXfs.push({ stackedDocTransform, width, height }); + //DocumentView is how the node will be rendered return ( <DocumentView ref={r => (dref = r || undefined)} @@ -296,7 +334,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} - dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} + dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} showTitle={this.props.childShowTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} @@ -357,6 +395,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return Math.min(childHeight, maxHeight, panelHeight); } + // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { runInAction(() => (this._cursor = 'grabbing')); setupMoveUpEvents( @@ -384,10 +423,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection @undoBatch @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + // Fairly confident that this is where the swapping of nodes in the various arrays happens const where = [de.x, de.y]; + // start at -1 until we're sure we want to add it to the column let dropInd = -1; let dropAfter = 0; if (de.complete.docDragData) { + // going to re-add the docs to the _docXFs based on position of where we just dropped this._docXfs.map((cd, i) => { const pos = cd .stackedDocTransform() @@ -402,11 +444,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }); const oldDocs = this.childDocs.length; if (super.onInternalDrop(e, de)) { + // check to see if we actually need anything to the new column of nodes (if droppedDocs != empty) const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; // if nothing was added to the end of the list, then presumably the dropped documents were already in the list, but possibly got reordered so we use them. const docs = this.childDocList; - DragManager.docsBeingDragged = []; + // still figuring out where to add the document if (docs && newDocs.length) { const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0); @@ -433,6 +476,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return true; } + /// an item from outside of Dash is being dropped onto this stacking view (e.g, a document from the file system) @undoBatch @action onExternalDrop = async (e: React.DragEvent): Promise<void> => { @@ -448,19 +492,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection targInd = i; } }); - super.onExternalDrop(e, {}, () => { - if (targInd !== -1) { - const newDoc = this.childDocs[this.childDocs.length - 1]; - const docs = this.childDocList; - if (docs) { - docs.splice(docs.length - 1, 1); - docs.splice(targInd, 0, newDoc); + super.onExternalDrop(e, {}, (docs: Doc[]) => { + if (targInd === -1) { + this.addDocument(docs); + } else { + const childDocs = this.childDocList; + if (childDocs) { + childDocs.splice(targInd, 0, ...docs); } } }); }; headings = () => Array.from(this.Sections); refList: any[] = []; + // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; @@ -512,6 +557,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); }; + // what a section looks like if we're in masonry. Shouldn't actually need to use this. sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined; @@ -556,6 +602,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); }; + /// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc) @action addGroup = (value: string) => { if (value && this.columnHeaders) { @@ -584,6 +631,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }; + // @computed get renderedSections() { TraceMobx(); let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index f3a798143..7b268cd49 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,31 +1,32 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { RichTextField } from "../../../fields/RichTextField"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { ImageField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DragManager } from "../../util/DragManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { EditableView } from "../EditableView"; -import "./CollectionStackingView.scss"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; -import { Id } from "../../../fields/FieldSymbols"; -const higflyout = require("@hig/flyout"); +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { RichTextField } from '../../../fields/RichTextField'; +import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import './CollectionStackingView.scss'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { Id } from '../../../fields/FieldSymbols'; +const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +// So this is how we are storing a column interface CSVFieldColumnProps { Document: Doc; DataDoc: Opt<Doc>; @@ -39,8 +40,9 @@ interface CSVFieldColumnProps { columnWidth: number; numGroupColumns: number; gridGap: number; - type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; + type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; + // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; @@ -51,17 +53,19 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { - @observable private _background = "inherit"; + @observable private _background = 'inherit'; private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); @observable _paletteOn = false; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; - @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; _ele: HTMLElement | null = null; + // This is likely similar to what we will be doing. Why do we need to make these refs? + // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { @@ -69,12 +73,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC this.props.observeHeight(ele); this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } - } + }; componentWillUnmount() { this.props.unobserveHeight(this._ele); } + //TODO: what is scripting? I found it in SetInPlace def but don't know what that is @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; @@ -83,10 +88,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC getValue = (value: string): any => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; - if (value.toLowerCase().indexOf("true") > -1) return true; - if (value.toLowerCase().indexOf("false") > -1) return false; + if (value.toLowerCase().indexOf('true') > -1) return true; + if (value.toLowerCase().indexOf('false') > -1) return false; return value; - } + }; @action headingChanged = (value: string, shiftDown?: boolean) => { @@ -95,7 +100,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } - this.props.docList.forEach(d => d[this.props.pivotField] = castedValue); + this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); if (this.props.headingObject) { this.props.headingObject.setHeading(castedValue.toString()); this._heading = this.props.headingObject.heading; @@ -103,17 +108,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC return true; } return false; - } + }; @action changeColumnColor = (color: string) => { this.props.headingObject?.setColor(color); this._color = color; - } + }; - @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4"); - @action pointerLeave = () => this._background = "inherit"; - textCallback = (char: string) => this.addNewTextDoc("-typed text-", false, true); + @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); + @action pointerLeave = () => (this._background = 'inherit'); + textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { @@ -121,71 +126,78 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const key = this.props.pivotField; const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); - const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + const maxHeading = this.props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; FormattedTextBox.SelectOnLoad = newDoc[Id]; - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " "; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; return this.props.addDocument?.(newDoc) || false; - } + }; @action deleteColumn = () => { - this.props.docList.forEach(d => d[this.props.pivotField] = undefined); + this.props.docList.forEach(d => (d[this.props.pivotField] = undefined)); if (this.props.columnHeaders && this.props.headingObject) { const index = this.props.columnHeaders.indexOf(this.props.headingObject); this.props.columnHeaders.splice(index, 1); } - } + }; @action collapseSection = () => { this.props.headingObject?.setCollapsed(!this.props.headingObject.collapsed); this.toggleVisibility(); - } + }; headerDown = (e: React.PointerEvent<HTMLDivElement>) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + //TODO: I think this is where I'm supposed to edit stuff startDrag = (e: PointerEvent, down: number[], delta: number[]) => { + // is MakeAlias a way to make a copy of a doc without rendering it? const alias = Doc.MakeAlias(this.props.Document); alias._width = this.props.columnWidth / (this.props.columnHeaders?.length || 1); alias._pivotField = undefined; let value = this.getValue(this._heading); - value = typeof value === "string" ? `"${value}"` : value; + value = typeof value === 'string' ? `"${value}"` : value; alias.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name }); if (alias.viewSpecScript) { DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); return true; } return false; - } + }; renderColorPicker = () => { - const gray = "#f1efeb"; + const gray = '#f1efeb'; const selected = this.props.headingObject ? this.props.headingObject.color : gray; - const colors = ["pink2", "purple4", "bluegreen1", "yellow4", "gray", "red2", "bluegreen7", "bluegreen5", "orange1"]; - return <div className="collectionStackingView-colorPicker"> - <div className="colorOptions"> - {colors.map(col => { - const palette = PastelSchemaPalette.get(col); - return <div className={"colorPicker" + (selected === palette ? " active" : "")} - style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; - })} + const colors = ['pink2', 'purple4', 'bluegreen1', 'yellow4', 'gray', 'red2', 'bluegreen7', 'bluegreen5', 'orange1']; + return ( + <div className="collectionStackingView-colorPicker"> + <div className="colorOptions"> + {colors.map(col => { + const palette = PastelSchemaPalette.get(col); + return <div className={'colorPicker' + (selected === palette ? ' active' : '')} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; + })} + </div> </div> - </div>; - } + ); + }; renderMenu = () => { - return <div className="collectionStackingView-optionPicker"> - <div className="optionOptions"> - <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div> + return ( + <div className="collectionStackingView-optionPicker"> + <div className="optionOptions"> + <div className={'optionPicker' + (true ? ' active' : '')} onClick={action(() => {})}> + Add options here + </div> + </div> </div> - </div >; - } + ); + }; @observable private collapsed: boolean = false; - private toggleVisibility = action(() => this.collapsed = !this.collapsed); + private toggleVisibility = action(() => (this.collapsed = !this.collapsed)); menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); @@ -193,42 +205,58 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const docItems: ContextMenuProps[] = []; const dataDoc = this.props.DataDoc || this.props.Document; - DocUtils.addDocumentCreatorMenuItems((doc) => { - FormattedTextBox.SelectOnLoad = doc[Id]; - return this.props.addDocument?.(doc); - }, this.props.addDocument, x, y, true); + DocUtils.addDocumentCreatorMenuItems( + doc => { + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.addDocument?.(doc); + }, + this.props.addDocument, + x, + y, + true + ); - Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => - docItems.push({ - description: ":" + fieldKey, event: () => { - const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); - if (created) { - if (this.props.Document.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(created, this.props.Document); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + return this.props.addDocument?.(created); } - return this.props.addDocument?.(created); - } - }, icon: "compress-arrows-alt" - })); - Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey => - docItems.push({ - description: ":" + fieldKey, event: () => { - const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); - if (created) { - const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; - if (container.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(created, container); - return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + }, + icon: 'compress-arrows-alt', + }) + ); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + } + return this.props.addDocument?.(created) || false; } - return this.props.addDocument?.(created) || false; - } - }, icon: "compress-arrows-alt" - })); - !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); - !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); - ContextMenu.Instance.setDefaultItem("::", (name: string): void => { - Doc.GetProto(this.props.Document)[name] = ""; - const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + }, + icon: 'compress-arrows-alt', + }) + ); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); + ContextMenu.Instance.setDefaultItem('::', (name: string): void => { + Doc.GetProto(this.props.Document)[name] = ''; + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); if (created) { if (this.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.Document); @@ -238,9 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC }); const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); ContextMenu.Instance.displayMenu(x, y, undefined, true); - } - - + }; @computed get innards() { TraceMobx(); @@ -249,39 +275,39 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin; const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); - const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - const headingView = this.props.headingObject ? - <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} + const evContents = heading ? heading : this.props?.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`; + const headingView = this.props.headingObject ? ( + <div + key={heading} + className="collectionStackingView-sectionHeader" + ref={this._headerRef} style={{ marginTop: this.props.yMargin, - width: (this.props.columnWidth) / - ((uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1)) || 1) + width: this.props.columnWidth / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1), }}> - <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> + <div className={'collectionStackingView-collapseBar' + (this.props.headingObject.collapsed === true ? ' active' : '')} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} - <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} - title={evContents === `NO ${key.toUpperCase()} VALUE` ? - `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} - style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}> - <EditableView - GetValue={() => evContents} - SetValue={this.headingChanged} - contents={evContents} - oneLine={true} - toggle={this.toggleVisibility} /> - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div + className="collectionStackingView-sectionHeader-subCont" + onPointerDown={this.headerDown} + title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''} + style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : 'inherit' }}> + <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} toggle={this.toggleVisibility} /> + {evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionColor"> - <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}> <FontAwesomeIcon icon="palette" size="lg" /> </button> - {this._paletteOn ? this.renderColorPicker() : (null)} + {this._paletteOn ? this.renderColorPicker() : null} </div> + )} + { + <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button> } - {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> - <FontAwesomeIcon icon="trash" size="lg" /> - </button>} - {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + {evContents === `NO ${key.toUpperCase()} VALUE` ? null : ( <div className="collectionStackingView-sectionOptions"> <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> @@ -289,65 +315,76 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </button> </Flyout> </div> - } + )} </div> - </div> : (null); + </div> + ) : null; const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `; const type = this.props.Document.type; - return <> - {this.props.Document._columnsHideIfEmpty ? (null) : headingView} - { - this.collapsed ? (null) : + return ( + <> + {this.props.Document._columnsHideIfEmpty ? null : headingView} + {this.collapsed ? null : ( <div> - <div key={`${heading}-stack`} className={`collectionStackingView-masonrySingle`} + <div + key={`${heading}-stack`} + className={`collectionStackingView-masonrySingle`} style={{ padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + margin: 'auto', + width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', - position: "relative", + position: 'relative', gridGap: this.props.gridGap, gridTemplateColumns: templatecols, - gridAutoRows: "0px" + gridAutoRows: '0px', }}> {this.props.renderChildren(this.props.docList)} </div> - {!this.props.chromeHidden ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}> + {!this.props.chromeHidden && type !== DocumentType.PRES ? ( + // TODO: this is the "new" button: see what you can work with here + // change cursor to pointer for this, and update dragging cursor + //TODO: there is a bug that occurs when adding a freeform document and trying to move it around + //TODO: would be great if there was additional space beyond the frame, so that you can actually see your + // bottom note + //TODO: ok, so we are using a single column, and this is it! + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10, marginLeft: 25 }}> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} - contents={"+ NEW"} + placeholder={"Type ':' for commands"} + contents={<FontAwesomeIcon icon={'plus'} />} toggle={this.toggleVisibility} - menuCallback={this.menuCallback} /> + menuCallback={this.menuCallback} + /> </div> - : null - } - - + ) : null} </div> - } - </>; + )} + </> + ); } - render() { TraceMobx(); const headings = this.props.headings(); const heading = this._heading; const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( - <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} + <div + className={'collectionStackingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} + key={heading} style={{ width: `${100 / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1)}%`, height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, - background: this._background + background: this._background, }} - ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> + ref={this.createColumnDropRef} + onPointerEnter={this.pointerEntered} + onPointerLeave={this.pointerLeave}> {this.innards} - </div > + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5479929bd..4227955ef 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -88,11 +88,11 @@ export function CollectionSubView<X>(moreProps?: X) { } collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []); + // child filters apply to the descendants of the documents in this collection childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; + // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; - IsFiltered = () => - this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined; searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); @@ -122,13 +122,11 @@ export function CollectionSubView<X>(moreProps?: X) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } - // console.log(Doc.ActiveDashboard._docFilters); - // if (!this.props.Document._docFilters && this.props.Document.currentFilter) { - // (this.props.Document.currentFilter as Doc).filterBoolean = (this.props.ContainingCollectionDoc?.currentFilter as Doc)?.filterBoolean; - // } const docsforFilter: Doc[] = []; childDocs.forEach(d => { - // if (DocUtils.Excluded(d, docFilters)) return; + // dragging facets + const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); + if (dragged && DragManager.docsBeingDragged.includes(d)) return false; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0; @@ -448,6 +446,7 @@ export function CollectionSubView<X>(moreProps?: X) { } this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); } + slowLoadDocuments = async ( files: File[] | string, options: DocumentOptions, @@ -458,11 +457,24 @@ export function CollectionSubView<X>(moreProps?: X) { clientY: number, addDocument: (doc: Doc | Doc[]) => boolean ) => { - const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 }); + // create placeholder docs + // inside placeholder docs have some func that + + let pileUpDoc = undefined; if (typeof files === 'string') { - generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options))); + const loading = Docs.Create.LoadingDocument(files, options); + generatedDocuments.push(loading); + loading.isLoading = true; + DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { - generatedDocuments.push(...(await DocUtils.uploadFilesToDocs(files, options))); + generatedDocuments.push( + ...files.map(file => { + const loading = Docs.Create.LoadingDocument(file, options); + loading.isLoading = true; + DocUtils.uploadFileToDoc(file, {}, loading); + return loading; + }) + ); } if (generatedDocuments.length) { // Creating a dash document @@ -478,7 +490,8 @@ export function CollectionSubView<X>(moreProps?: X) { if (completed) completed(set); else { if (isFreeformView && generatedDocuments.length > 1) { - addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!); + pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!; + addDocument(pileUpDoc); } else { generatedDocuments.forEach(addDocument); } @@ -490,7 +503,6 @@ export function CollectionSubView<X>(moreProps?: X) { alert('Document upload failed - possibly an unsupported file type.'); } } - disposer(); }; } @@ -503,5 +515,4 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes' import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { OverlayView } from '../OverlayView'; import { CollectionView, CollectionViewProps } from './CollectionView'; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 93523a6cf..c0561e42c 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,6 +1,5 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; - .collectionTreeView-container { transform-origin: top left; height: 100%; @@ -28,7 +27,7 @@ list-style: none; padding-left: $TREE_BULLET_WIDTH; margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason.... - > .contentFittingDocumentView { + > .contentFittingDocumentView { width: unset; height: unset; } @@ -39,7 +38,7 @@ .no-indent { padding-left: 0; - width: max-content; + //width: max-content; } .no-indent-outline { @@ -85,7 +84,7 @@ width: 100%; height: max-content; .contentFittingDocumentView { - display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason) + display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason) } } @@ -114,4 +113,4 @@ padding-left: 3px; padding-right: 3px; padding-bottom: 2px; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 809a73a77..dce792d19 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -235,7 +235,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight PanelWidth={this.documentTitleWidth} PanelHeight={this.documentTitleHeight} - scaling={returnOne} + NativeDimScaling={returnOne} onKey={this.onKey} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -351,7 +351,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return Doc.NativeHeight(this.Document, undefined, true); } - @computed get contentScaling() { + /// scale factor for tree view so that it will fit within it's panel bounds + @computed get nativeDimScaling() { const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; @@ -365,7 +366,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1); + panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.NativeDimScaling?.() || 1); addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; @@ -389,9 +390,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree <div className="collectionTreeView-container" style={{ - transform: this.outlineMode ? `scale(${this.contentScaling})` : '', + transform: this.outlineMode ? `scale(${this.nativeDimScaling})` : '', paddingLeft: `${this.marginX()}px`, - width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : '', + width: this.outlineMode ? `calc(${100 / this.nativeDimScaling}%)` : '', }} onContextMenu={this.onContextMenu}> {!this.buttonMenu && !this.noviceExplainer ? null : ( diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2ab5f6247..1ee77d4ce 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -26,6 +26,7 @@ import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; +import { CollectionNoteTakingView } from './CollectionNoteTakingView'; import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; import { CollectionStackingView } from './CollectionStackingView'; @@ -125,6 +126,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab case CollectionViewType.Carousel: return <CollectionCarouselView key="collview" {...props} />; case CollectionViewType.Carousel3D: return <CollectionCarousel3DView key="collview" {...props} />; case CollectionViewType.Stacking: return <CollectionStackingView key="collview" {...props} />; + case CollectionViewType.NoteTaking: return <CollectionNoteTakingView key="collview" {...props} />; case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />; case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />; case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />; @@ -141,6 +143,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab subItems.push({ description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' }); subItems.push({ description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' }); subItems.push({ description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' }); + subItems.push({ description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' }); subItems.push({ description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' }); subItems.push({ description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' }); subItems.push({ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }); @@ -225,7 +228,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab }, }) ); - DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null).data).forEach(childClick => + DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data).forEach(childClick => onClicks.push({ description: `Set child ${childClick.title}`, icon: 'edit', diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 7614154c0..30fa33b52 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -212,15 +212,12 @@ export class TabDocView extends React.Component<TabDocViewProps> { @action public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; - const batch = UndoManager.StartBatch('pinning doc'); let curPres = Doc.ActivePresentation console.log(curPres) - // let curPres = Doc.ActiveDashboard!['presentation'] if (!curPres) { curPres = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); Doc.ActivePresentation = curPres - // Doc.ActiveDashboard!['presentation'] = curPres } curPres && @@ -246,46 +243,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header 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 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 pinWithView option set then update scale and x / y props of slide - if (pinProps?.pinWithView) { - const viewProps = pinProps.pinWithView; - pinDoc.presPinView = true; - pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2; - pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2; - pinDoc.presPinViewScale = viewProps.scale; - pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left + viewProps.bounds.width, viewProps.bounds.top + viewProps.bounds.height]); - } - if (pinProps?.pinDocView) { - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking; - const pannable: boolean = (pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG; - if (scrollable) { - const scroll = doc._scrollTop; - pinDoc.presPinView = true; - pinDoc.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) { - pinDoc.presPinView = true; - pinDoc.presStartTime = doc._currentTimecode; - pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1; - } else if (pannable) { - pinDoc.presPinView = true; - pinDoc.presPinViewX = pinDoc._panX; - pinDoc.presPinViewY = pinDoc._panY; - pinDoc.presPinViewScale = pinDoc._viewScale; - const pw = NumCast(pinProps.panelWidth); - const ph = NumCast(pinProps.panelHeight); - const ps = NumCast(pinDoc._viewScale); - if (pw && ph && ps) { - pinDoc.contentBounds = new List<number>([NumCast(pinDoc.panX) - pw / 2 / ps, NumCast(pinDoc.panY) - ph / 2 / ps, NumCast(pinDoc.panX) + pw / 2 / ps, NumCast(pinDoc.panY) + ph / 2 / ps]); - } - } else if (doc.type === DocumentType.COMPARISON) { - const width = doc._clipWidth; - pinDoc.presPinClipWidth = width; - pinDoc.presPinView = true; - } - } + + PresBox.pinDocView(pinDoc, pinProps, doc); pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)'); Doc.AddDocToList(curPres, 'data', pinDoc, presSelected); if (!pinProps?.audioRange && duration !== undefined) { @@ -295,31 +257,33 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presEndTime = NumCast(doc.clipEnd, duration); } //save position - if (pinProps?.setPosition || pinDoc.isInkMask) { - pinDoc.setPosition = true; - pinDoc.y = doc.y; - pinDoc.x = doc.x; + if (pinProps?.activeFrame !== undefined) { + pinDoc.presActiveFrame = pinProps?.activeFrame; + pinDoc.title = doc.title + ' (move)'; + pinDoc.presMovement = PresMovement.Pan; + } + if (pinDoc.isInkMask) { pinDoc.presHideAfter = true; pinDoc.presHideBefore = true; - pinDoc.title = doc.title + ' (move)'; pinDoc.presMovement = PresMovement.None; } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - PresBox.Instance?._selectedArray.clear(); - pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array + PresBox.Instance?.clearSelectedArray(); + pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array }); - if ( - !Array.from(CollectionDockingView.Instance.tabMap) - .map(d => d.DashDoc) - .includes(curPres) - ) { - const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); - if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); - CollectionDockingView.AddSplit(curPres, 'right'); - setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things - } - setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs - } + if ( + CollectionDockingView.Instance && + !Array.from(CollectionDockingView.Instance.tabMap) + .map(d => d.DashDoc) + .includes(curPres) + ) { + const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); + if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); + CollectionDockingView.AddSplit(curPres, 'right'); + setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things + } + setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs + } componentDidMount() { new _global.ResizeObserver( @@ -405,7 +369,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { focusFunc = (doc: Doc, options?: DocFocusOptions) => { const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; if (shrinkwrap && this._document) { - const focusSpeed = 1000; + const focusSpeed = NumCast(this._document.focusSpeed, 500); shrinkwrap(); this._document._viewTransition = `transform ${focusSpeed}ms`; setTimeout( @@ -427,7 +391,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { ScreenToLocalTransform = () => { this._forceInvalidateScreenToLocal; const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY); + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY) ?? Transform.Identity(); }; PanelWidth = () => this._panelWidth; PanelHeight = () => this._panelHeight; @@ -456,9 +420,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} styleProvider={DefaultStyleProvider} - docFilters={CollectionDockingView.Instance.childDocFilters} - docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} addDocument={undefined} removeDocument={this.remDocTab} addDocTab={this.addDocTab} @@ -631,9 +595,9 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { styleProvider={TabMinimapView.miniStyleProvider} addDocTab={this.props.addDocTab} pinToPres={TabDocView.PinDoc} - docFilters={CollectionDockingView.Instance.childDocFilters} - docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} fitContentsToBox={returnTrue} /> <div className="miniOverlay" onPointerDown={this.miniDown}> diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index f587dbbf6..ce87e6f89 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .treeView-label { max-height: 1.5em; @@ -21,7 +21,7 @@ } .treeView-bulletIcons { - // width: $TREE_BULLET_WIDTH; + // width: $TREE_BULLET_WIDTH; width: 100%; height: 100%; @@ -101,6 +101,9 @@ .treeView-border { display: flex; overflow: hidden; + > ul { + width: 100%; + } } .treeView-border { @@ -118,7 +121,6 @@ } .formattedTextBox-cont { - .formattedTextbox-sidebar, .formattedTextbox-sidebar-inking { overflow: visible !important; @@ -144,12 +146,12 @@ pointer-events: all; cursor: pointer; - >svg { + > svg { margin-left: 0.25rem; margin-right: 0.25rem; } - >svg { + > svg { //display: none; opacity: 0; pointer-events: none; @@ -176,8 +178,7 @@ } .treeView-rightButtons { - - >svg, + > svg, .styleProvider-treeView-icon { display: inherit; opacity: unset; @@ -196,4 +197,4 @@ .treeView-header-inside { border: black 1px solid; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 5a2103e98..aa1330762 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -403,7 +403,7 @@ export class TreeView extends React.Component<TreeViewProps> { const aspect = Doc.NativeAspect(layoutDoc); if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]()); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth())); - return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); + return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); }; docHeight = () => { const layoutDoc = this.layoutDoc; @@ -514,7 +514,7 @@ export class TreeView extends React.Component<TreeViewProps> { rtfWidth = () => { const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; - return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1); + return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1); }; rtfHeight = () => { const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; @@ -921,7 +921,6 @@ export class TreeView extends React.Component<TreeViewProps> { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.treeView.props.Document} - ContentScaling={returnOne} /> ); @@ -992,7 +991,6 @@ export class TreeView extends React.Component<TreeViewProps> { hideResizeHandles={this.props.treeView.outlineMode} onClick={this.onChildClick} focus={this.refocus} - ContentScaling={returnOne} onKey={this.onKeyDown} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index a0ebe4cdc..89cc22d07 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -34,6 +34,7 @@ export interface PoolData { zIndex?: number; width?: number; height?: number; + backgroundColor?: string; color?: string; opacity?: number; transition?: string; @@ -45,6 +46,7 @@ export interface PoolData { export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; + inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden } function toLabel(target: FieldResult<Field>) { if (typeof target === 'number' || Number(target)) { @@ -393,8 +395,10 @@ function normalizeResults( 0 ); aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x); - const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); - let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? panelDim[1] / (aggBounds.b - aggBounds.y) : wscale; + const width = aggBounds.r - aggBounds.x === 0 ? 1 : aggBounds.r - aggBounds.x; + const height = aggBounds.b - aggBounds.y === 0 ? 1 : aggBounds.b - aggBounds.y; + const wscale = panelDim[0] / width; + let scale = wscale * height > panelDim[1] ? panelDim[1] / height : wscale; if (Number.isNaN(scale)) scale = 1; Array.from(docMap.entries()) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index d979ef961..bf9de6760 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -195,7 +195,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; const aleft = this.visibleX(adiv); const bleft = this.visibleX(bdiv); - const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; + const aclipped = aleft !== a.left || atop !== a.top; + const bclipped = bleft !== b.left || btop !== b.top; + if (aclipped && bclipped) return undefined; + const clipped = aclipped || bclipped; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]]; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 8720c9097..b8344dc0c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -5,11 +5,14 @@ import { DocumentManager } from '../../../util/DocumentManager'; import './CollectionFreeFormLinksView.scss'; import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView'; import React = require('react'); +import { LightboxView } from '../../LightboxView'; @observer export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> { @computed get uniqueConnections() { - return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); + return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) + .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath))) + .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 79e063f7f..d80fcdfc3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables'; .collectionfreeformview-none { position: inherit; @@ -20,8 +20,24 @@ pointer-events: none; } +.collectionfreeformview-mask-empty, +.collectionfreeformview-mask { + z-index: 5000; + width: $INK_MASK_SIZE; + height: $INK_MASK_SIZE; + transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF); + pointer-events: none; + position: absolute; + background-color: transparent; + transition: background-color 1s ease 0s; +} +.collectionfreeformview-mask { + mix-blend-mode: multiply; + background-color: rgba(0, 0, 0, 0.7); +} + .collectionfreeformview-viewdef { - >.collectionFreeFormDocumentView-container { + > .collectionFreeFormDocumentView-container { pointer-events: none; .contentFittingDocumentDocumentView-previewDoc { @@ -210,13 +226,13 @@ } } - .collectionfreeformview>.jsx-parser { + .collectionfreeformview > .jsx-parser { position: inherit; height: 100%; width: 100%; } - >.jsx-parser { + > .jsx-parser { z-index: 0; } @@ -268,6 +284,6 @@ .pullpane-indicator { z-index: 99999; - background-color: rgba($color: #000000, $alpha: .4); + background-color: rgba($color: #000000, $alpha: 0.4); position: absolute; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4174661d8..039407e77 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -23,7 +23,6 @@ import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { HistoryUtil } from '../../../util/History'; import { InteractionUtils } from '../../../util/InteractionUtils'; -import { RecordingApi } from '../../../util/RecordingApi'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -35,7 +34,7 @@ import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariable import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; @@ -45,7 +44,6 @@ import { PresBox } from '../../nodes/trails/PresBox'; import { VideoBox } from '../../nodes/VideoBox'; import { CreateImage } from '../../nodes/WebBoxRenderer'; import { StyleProp } from '../../StyleProvider'; -import { CollectionDockingView } from '../CollectionDockingView'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; import { TabDocView } from '../TabDocView'; @@ -54,12 +52,11 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); -import e = require('connect-flash'); export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; - childPointerEvents?: boolean; + childPointerEvents?: string; scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; @@ -110,19 +107,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @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 _pullCoords: number[] = [0, 0]; - @observable _pullDirection: string = ''; @observable _showAnimTimeline = false; @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); @observable _marqueeViewRef = React.createRef<MarqueeView>(); - @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @computed get views() { - return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele); + const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele); + if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>); + return renderableEles; } @computed get fitToContentVals() { return { @@ -131,7 +128,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } @computed get fitContentsToBox() { - return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; + return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay; } @computed get contentBounds() { const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); @@ -151,11 +148,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } @computed get cachedCenteringShiftX(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -185,8 +182,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame)); } }; - @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); - getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); @@ -236,11 +231,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newBoxes = newBox instanceof Doc ? [newBox] : newBox; for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { - const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field}-indexed`]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field]); + const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}-indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]); delete newBox.activeFrame; - CollectionFreeFormDocumentView.animFields.forEach((field, i) => field !== 'opacity' && (newBox[field] = vals[i])); + CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i])); } } if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { @@ -272,16 +267,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1)); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); - const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)]; + const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); - const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - vals.x = x + (vals.x || 0) - dropPos[0]; - vals.y = y + (vals.y || 0) - dropPos[1]; - vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined; + const pvals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); // get filled in values (uses defaults when not value is specified) for position + const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000), false); // get non-default values for everything else + vals.x = x + NumCast(pvals.x) - dropPos[0]; + vals.y = y + NumCast(pvals.y) - dropPos[1]; CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals); } else { d.x = x + NumCast(d.x) - dropPos[0]; @@ -463,20 +458,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => { let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 - if (property !== StyleProp.BackgroundColor) return styleProp; - const cluster = NumCast(doc?.cluster); - if (this.Document._useClusters) { - if (this._clusterSets.length <= cluster) { - setTimeout(() => doc && this.updateCluster(doc)); - } else { - // choose a cluster color from a palette - const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; - styleProp = colors[cluster % colors.length]; - const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); - // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set?.map(s => (styleProp = StrCast(s.backgroundColor))); - } - } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray"; + switch (property) { + case StyleProp.BackgroundColor: + const cluster = NumCast(doc?.cluster); + if (this.Document._useClusters) { + if (this._clusterSets.length <= cluster) { + setTimeout(() => doc && this.updateCluster(doc)); + } else { + // choose a cluster color from a palette + const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; + styleProp = colors[cluster % colors.length]; + const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); + // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document + set?.map(s => (styleProp = StrCast(s.backgroundColor))); + } + } + break; + } return styleProp; }; @@ -497,7 +495,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const currentCol = DocListCast(this.rootDoc.currentInkDoc); const rootDocList = DocListCast(this.rootDoc.data); currentCol.push(rootDocList[rootDocList.length - 1]); - console.log(currentCol); this._batch?.end(); } @@ -522,17 +519,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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: - document.addEventListener('pointermove', this.onEraserMove); - document.addEventListener('pointerup', this.onEraserUp); this._batch = UndoManager.StartBatch('collectionErase'); - e.stopPropagation(); - e.preventDefault(); + setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); break; case InkTool.None: if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - document.addEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointerup', this.onPointerUp); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false); } break; } @@ -571,13 +564,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, { - title: 'ink stroke', - x: B.x - ActiveInkWidth() / 2, - y: B.y - ActiveInkWidth() / 2, - _width: B.width + ActiveInkWidth(), - _height: B.height + ActiveInkWidth(), - }); + const inkDoc = Docs.Create.InkDocument( + ActiveInkColor(), + Doc.ActiveTool, + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + ActiveInkBezierApprox(), + ActiveFillColor(), + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + points, + ActiveIsInkMask(), + { + title: 'ink stroke', + x: B.x - ActiveInkWidth() / 2, + y: B.y - ActiveInkWidth() / 2, + _width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + _height: B.height + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, + } + ); if (Doc.ActiveTool === InkTool.Write) { this.unprocessedDocs.push(inkDoc); CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); @@ -685,23 +690,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action onEraserUp = (e: PointerEvent): void => { - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - document.removeEventListener('pointermove', this.onEraserMove); - document.removeEventListener('pointerup', this.onEraserUp); - this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); - this._deleteList = []; - this._batch?.end(); - } - }; - - @action - onPointerUp = (e: PointerEvent): void => { - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); - this.removeMoveListeners(); - this.removeEndListeners(); - } + this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); + this._deleteList = []; + this._batch?.end(); }; onClick = (e: React.MouseEvent) => { @@ -739,46 +730,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety. */ @action - onEraserMove = (e: PointerEvent) => { + onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { const currPoint = { X: e.clientX, Y: e.clientY }; - this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => { + this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { this._deleteList.push(intersect.inkView); SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1'); SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black'); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. - !e.shiftKey && + if (!e.shiftKey) { this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment => GestureOverlay.Instance.dispatchGesture( GestureUtils.Gestures.Stroke, segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) ) ); + } // Lower ink opacity to give the user a visual indicator of deletion. intersect.inkView.layoutDoc.opacity = 0.5; + intersect.inkView.layoutDoc.dontIntersect = true; } }); - this._lastX = currPoint.X; - this._lastY = currPoint.Y; - - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); + return false; }; @action - onPointerMove = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; + onPointerMove = (e: PointerEvent): boolean => { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false; if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { Doc.ActiveTool = InkTool.None; if (this.props.isContentActive(true)) e.stopPropagation(); } else if (!e.cancelBubble) { if (this.tryDragCluster(e, this._hitCluster)) { - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); + return true; } else this.pan(e); - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); } + return false; }; /** @@ -788,6 +775,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) }; const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; + return this.childDocs .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) @@ -875,7 +863,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const tVals: number[] = []; // Iterating through all ink strokes in the current freeform collection. this.childDocs - .filter(doc => doc.type === DocumentType.INK) + .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) .forEach(doc => { const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke; const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; @@ -906,7 +894,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); return; } // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected' @@ -954,13 +941,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY); - if (!this._pullDirection) { - // if we are not bezel movement - this.pan({ clientX: centerX, clientY: centerY }); - } else { - this._pullCoords = [centerX, centerY]; - } - this._lastX = centerX; this._lastY = centerY; } @@ -985,24 +965,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this._lastX = centerX; this._lastY = centerY; - const screenBox = this._mainCont?.getBoundingClientRect(); - - // determine if we are using a bezel movement - if (screenBox) { - if (screenBox.right - centerX < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'right'; - } else if (centerX - screenBox.left < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'left'; - } else if (screenBox.bottom - centerY < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'bottom'; - } else if (centerY - screenBox.top < 100) { - this._pullCoords = [centerX, centerY]; - this._pullDirection = 'top'; - } - } this.removeMoveListeners(); this.addMoveListeners(); @@ -1014,19 +976,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; cleanUpInteractions = () => { - switch (this._pullDirection) { - case 'left': - case 'right': - case 'top': - case 'bottom': - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: 'New Collection' }), this._pullDirection); - } - - this._pullDirection = ''; - this._pullCoords = [0, 0]; - - document.removeEventListener('pointermove', this.onPointerMove); - document.removeEventListener('pointerup', this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); }; @@ -1055,7 +1004,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._Transform || (this.layoutDoc._fitWidth && this.layoutDoc.nativeHeight) || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; + if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); @@ -1068,17 +1017,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { - // set the current respective FFview to the tab being panned. - Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this); - // TODO: make this based off the specific recording FFView - Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this); - - // TODO: zzz + michael to figure out this merge in case of strange behaviour - // if (Doc.UserDoc()?.presentationMode === 'watching') { - // RecordingApi.Instance.pauseVideoAndMovements(); - // Doc.UserDoc().presentationMode = 'none'; - // // RecordingApi.Instance.pauseMovements() - // } // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code. if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction(); @@ -1202,12 +1140,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection - newState.initializers![this.Document[Id]] = { panX: panX, panY: panY }; + newState.initializers![this.Document[Id]] = { panX, panY }; HistoryUtil.pushState(newState); } // focus on the document in the collection const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); - const focusSpeed = options?.instant ? 0 : didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0; + const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { scale && (this.Document[this.scaleFieldKey] = scale); @@ -1226,8 +1164,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document._panY = restoreState.panY; this.Document[this.scaleFieldKey] = restoreState.scale; } - runInAction(() => (this._viewTransition = 0)); } + runInAction(() => (this._viewTransition = 0)); return resetView; }; const xf = !cantTransform @@ -1252,17 +1190,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (scale) { const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context - const newScale = Math.min(maxZoom, (1 / (this.contentScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); + const newScale = Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); return { panX: this.props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, panY: this.props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, scale: newScale, }; } - const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1); - const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1); - const cx = NumCast(this.layoutDoc._panX); - const cy = NumCast(this.layoutDoc._panY); + + const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth(); + const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight(); + const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1); + const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1); + const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0); + const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0); const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) { return { @@ -1272,8 +1213,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } return { - panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), + panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), + panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), }; }; @@ -1302,7 +1243,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; pointerEvents = () => { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ? 'all' : this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.(); + const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); return pointerEvents; }; getChildDocView(entry: PoolData) { @@ -1374,22 +1315,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { - const layoutDoc = Doc.Layout(params.pair.layout); - const { z, color, zIndex } = params.pair.layout; - const { x, y, opacity } = - this.Document._currentFrame === undefined - ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) } - : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame)); + const curFrame = Cast(this.Document._currentFrame, 'number'); + const childDoc = params.pair.layout; + const childDocLayout = Doc.Layout(childDoc); + const { z, zIndex } = childDoc; + const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame); + const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame); return { x: NumCast(x), y: NumCast(y), z: Cast(z, 'number'), - color: StrCast(color), + color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), + backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor), + opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), - transition: StrCast(layoutDoc.dataTransition), - opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number', null), - width: Cast(layoutDoc._width, 'number'), - height: Cast(layoutDoc._height, 'number'), + width: Cast(childDocLayout._width, 'number'), + height: Cast(childDocLayout._height, 'number'), + transition: StrCast(childDocLayout.dataTransition), pair: params.pair, replica: '', }; @@ -1504,7 +1446,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (const entry of array) { const lastPos = this._cachedPool.get(entry[0]); // last computed pos const newPos = entry[1]; - if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) { + if ( + !lastPos || + newPos.color !== lastPos.color || + newPos.backgroundColor !== lastPos.backgroundColor || + newPos.opacity !== lastPos.opacity || + newPos.x !== lastPos.x || + newPos.y !== lastPos.y || + newPos.z !== lastPos.z || + newPos.zIndex !== lastPos.zIndex || + newPos.transition !== lastPos.transition + ) { this._layoutPoolData.set(entry[0], newPos); } if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { @@ -1521,6 +1473,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica), + inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1, }) ); @@ -1573,11 +1526,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection setTimeout( action(() => { this._firstRender = false; - this._disposers.layoutComputation = reaction( - () => this.doLayoutComputation, - elements => (this._layoutElements = elements || []), - { fireImmediately: true, name: 'doLayout' } - ); this._marqueeRef.current?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); @@ -1594,10 +1542,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; const pbounds = { - x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], - y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], - r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], - b: (cbounds.b - p[1]) * this.zoomScaling() + c[1], + x: cbounds.x - p[0] + c[0], + y: cbounds.y - p[1] + c[1], + r: cbounds.r - p[0] + c[0], + b: cbounds.b - p[1] + c[1], }; this.layoutDoc._width = pbounds.r - pbounds.x; this.layoutDoc._height = pbounds.b - pbounds.y; @@ -1609,6 +1557,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }, { fireImmediately: true } ); + + this._disposers.layoutComputation = reaction( + () => this.doLayoutComputation, + elements => (this._layoutElements = elements || []), + { fireImmediately: true, name: 'doLayout' } + ); }) ); } @@ -1632,9 +1586,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(img); + if (newCan) { + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(img); + } } } } @@ -1888,7 +1844,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @computed get placeholder() { return ( <div className="collectionfreeformview-placeholder" style={{ background: StrCast(this.Document.backgroundColor) }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()}</span> </div> ); } @@ -1902,6 +1858,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined} nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} + slowLoadDocuments={this.slowLoadDocuments} trySelectCluster={this.trySelectCluster} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} @@ -1910,7 +1867,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}> + <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}> {this.layoutDoc._backgroundGridShow ? ( <div> <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!? @@ -1944,13 +1901,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } - @computed get contentScaling() { + @computed get nativeDimScaling() { if (this._firstRender || (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent)) return 0; const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; - return wscale < hscale ? wscale : hscale; + return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale; } private groupDropDisposer?: DragManager.DragDropDisposer; @@ -1984,22 +1941,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) ? 'all' : (this.props.pointerEvents?.() as any), - transform: `scale(${this.contentScaling || 1})`, - width: `${100 / (this.contentScaling || 1)}%`, - height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.contentScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + transform: `scale(${this.nativeDimScaling || 1})`, + width: `${100 / (this.nativeDimScaling || 1)}%`, + height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.nativeDimScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> {this._firstRender ? this.placeholder : this.marqueeView} {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} - <div - className={'pullpane-indicator'} - style={{ - display: this._pullDirection ? 'block' : 'none', - top: clientRect ? (this._pullDirection === 'bottom' ? this._pullCoords[1] - clientRect.y : 0) : 'auto', - left: clientRect ? (this._pullDirection === 'right' ? this._pullCoords[0] - clientRect.x : 0) : 'auto', - width: clientRect ? (this._pullDirection === 'left' ? this._pullCoords[0] - clientRect.left : this._pullDirection === 'right' ? clientRect.right - this._pullCoords[0] : clientRect.width) : 0, - height: clientRect ? (this._pullDirection === 'top' ? this._pullCoords[1] - clientRect.top : this._pullDirection === 'bottom' ? clientRect.bottom - this._pullCoords[1] : clientRect.height) : 0, - }}></div> { // uncomment to show snap lines <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}> @@ -2269,3 +2217,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); +ScriptingGlobals.add(function pinWithView(readOnly: boolean) { + !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() })); +}); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 41e4d6b6a..e0f5cbe5b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -1,16 +1,14 @@ - .marqueeView { position: inherit; - top:0; - left:0; - width:100%; - height:100%; + top: 0; + left: 0; + width: 100%; + height: 100%; overflow: hidden; border-radius: inherit; user-select: none; } - .marqueeView:focus-within { overflow: hidden; } @@ -22,13 +20,13 @@ border-color: black; pointer-events: none; .marquee-legend { - bottom:-18px; - left:0; + bottom: -18px; + left: 0; position: absolute; font-size: 9; - white-space:nowrap; + white-space: nowrap; } .marquee-legend::after { - content: "Press <space> for lasso" + content: 'Press <space> for lasso'; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4513ffb39..58a00bbac 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -40,6 +40,16 @@ interface MarqueeViewProps { nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + slowLoadDocuments: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise<void>; } export interface MarqueeViewBounds { @@ -277,8 +287,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const hideMarquee = () => { this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); - document.removeEventListener('pointerdown', hideMarquee); - document.removeEventListener('wheel', hideMarquee); + document.removeEventListener('pointerdown', hideMarquee, true); + document.removeEventListener('wheel', hideMarquee, true); }; if (PresBox.startMarquee) { this.pinWithView(); @@ -292,8 +302,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; - document.addEventListener('pointerdown', hideMarquee); - document.addEventListener('wheel', hideMarquee); + document.addEventListener('pointerdown', hideMarquee, true); + document.addEventListener('wheel', hideMarquee, true); } else { this.hideMarquee(); } @@ -330,7 +340,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { - PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } this.clearSelection(); } @@ -356,14 +366,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }; @action - showMarquee = () => { - this._visible = true; - }; - + showMarquee = () => (this._visible = true); @action - hideMarquee = () => { - this._visible = false; - }; + hideMarquee = () => (this._visible = false); @undoBatch @action diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 4e4c33446..9468c5f06 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -1,6 +1,6 @@ import { action, computed, Lambda, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -15,8 +15,8 @@ import { ContextMenuProps } from '../../ContextMenuItem'; import { DocumentView } from '../../nodes/DocumentView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionGridView.scss"; -import Grid, { Layout } from "./Grid"; +import './CollectionGridView.scss'; +import Grid, { Layout } from './Grid'; @observer export class CollectionGridView extends CollectionSubView() { @@ -29,50 +29,76 @@ export class CollectionGridView extends CollectionSubView() { onChildClickHandler = () => ScriptCast(this.Document.onChildClick); - @computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); } - @computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; } - // sets the default width and height of the grid nodes - @computed get defaultW() { return NumCast(this.props.Document.gridDefaultW, 2); } - @computed get defaultH() { return NumCast(this.props.Document.gridDefaultH, 2); } + @computed get numCols() { + return NumCast(this.props.Document.gridNumCols, 10); + } + @computed get rowHeight() { + return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; + } + // sets the default width and height of the grid nodes + @computed get defaultW() { + return NumCast(this.props.Document.gridDefaultW, 2); + } + @computed get defaultH() { + return NumCast(this.props.Document.gridDefaultH, 2); + } - @computed get colWidthPlusGap() { return (this.props.PanelWidth() - this.margin) / this.numCols; } - @computed get rowHeightPlusGap() { return this.rowHeight + this.margin; } + @computed get colWidthPlusGap() { + return (this.props.PanelWidth() - this.margin) / this.numCols; + } + @computed get rowHeightPlusGap() { + return this.rowHeight + this.margin; + } - @computed get margin() { return NumCast(this.props.Document.margin, 10); } // sets the margin between grid nodes + @computed get margin() { + return NumCast(this.props.Document.margin, 10); + } // sets the margin between grid nodes - @computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized - @computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized + @computed get flexGrid() { + return BoolCast(this.props.Document.gridFlex, true); + } // is grid static/flexible i.e. whether nodes be moved around and resized + @computed get compaction() { + return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, 'vertical')); + } // is grid static/flexible i.e. whether nodes be moved around and resized /** * Sets up the listeners for the list of documents and the reset button. */ componentDidMount() { - this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => { - const newLayouts: Layout[] = []; - const oldLayouts = this.savedLayoutList; - pairs.forEach((pair, i) => { - const existing = oldLayouts.find(l => l.i === pair.layout[Id]); - if (existing) newLayouts.push(existing); - else { - if (Object.keys(this.dropLocation).length) { // external drop - this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number, y: number }, !this.flexGrid)); - this.dropLocation = {}; - } - else { // internal drop - this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + this._changeListenerDisposer = reaction( + () => this.childLayoutPairs, + pairs => { + const newLayouts: Layout[] = []; + const oldLayouts = this.savedLayoutList; + pairs.forEach((pair, i) => { + const existing = oldLayouts.find(l => l.i === pair.layout[Id]); + if (existing) newLayouts.push(existing); + else { + if (Object.keys(this.dropLocation).length) { + // external drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number; y: number }, !this.flexGrid)); + this.dropLocation = {}; + } else { + // internal drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + } } - } - }); - pairs?.length && this.setLayoutList(newLayouts); - }, { fireImmediately: true }); + }); + pairs?.length && this.setLayoutList(newLayouts); + }, + { fireImmediately: true } + ); // updates the layouts if the reset button has been clicked - this._resetListenerDisposer = reaction(() => this.props.Document.gridResetLayout, (reset) => { - if (reset && this.flexGrid) { - this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index)))); + this._resetListenerDisposer = reaction( + () => this.props.Document.gridResetLayout, + reset => { + if (reset && this.flexGrid) { + this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index)))); + } + this.props.Document.gridResetLayout = false; } - this.props.Document.gridResetLayout = false; - }); + ); } /** @@ -85,15 +111,15 @@ export class CollectionGridView extends CollectionSubView() { /** * @returns the default location of the grid node (i.e. when the grid is static) - * @param index + * @param index */ - unflexedPosition(index: number): Omit<Layout, "i"> { + unflexedPosition(index: number): Omit<Layout, 'i'> { return { x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, - static: true + static: true, }; } @@ -110,9 +136,9 @@ export class CollectionGridView extends CollectionSubView() { /** * Creates a layout object for a grid item */ - makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { - return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }); - } + makeLayoutItem = (doc: Doc, pos: { x: number; y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { + return { i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }; + }; /** * Adds a layout to the list of layouts. @@ -122,16 +148,16 @@ export class CollectionGridView extends CollectionSubView() { f !== -1 && layouts.splice(f, 1); layouts.push(layout); return layouts; - } + }; /** - * @returns the transform that will correctly place the document decorations box. + * @returns the transform that will correctly place the document decorations box. */ private lookupIndividualTransform = (layout: Layout) => { const xypos = this.flexGrid ? layout : this.unflexedPosition(this.renderedLayoutList.findIndex(l => l.i === layout.i)); const pos = { x: xypos.x * this.colWidthPlusGap + this.margin, y: xypos.y * this.rowHeightPlusGap + this.margin - this._scroll }; return this.props.ScreenToLocalTransform().translate(-pos.x, -pos.y); - } + }; /** * @returns the layout list converted from JSON @@ -147,26 +173,32 @@ export class CollectionGridView extends CollectionSubView() { this.props.Document.gridLayoutString = JSON.stringify(layouts); } + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + isChildContentActive = () => (this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined); /** - * - * @param layout + * + * @param layout * @param dxf the x- and y-translations of the decorations box as a transform i.e. this.lookupIndividualTransform - * @param width - * @param height + * @param width + * @param height * @returns the `ContentFittingDocumentView` of the node */ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { - return <DocumentView - {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - PanelWidth={width} - PanelHeight={height} - ScreenToLocalTransform={dxf} - onClick={this.onChildClickHandler} - renderDepth={this.props.renderDepth + 1} - dontCenter={this.props.Document.centerY ? "" : "y"} - />; + return ( + <DocumentView + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + isContentActive={this.isChildContentActive} + PanelWidth={width} + PanelHeight={height} + ScreenToLocalTransform={dxf} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + dontCenter={this.props.Document.centerY ? '' : 'y'} + /> + ); } /** @@ -176,7 +208,7 @@ export class CollectionGridView extends CollectionSubView() { @action setLayout = (layoutArray: Layout[]) => { // for every child in the collection, check to see if there's a corresponding grid layout object and - // updated layout object. If both exist, which they should, update the grid layout object from the updated object + // updated layout object. If both exist, which they should, update the grid layout object from the updated object if (this.flexGrid) { const savedLayouts = this.savedLayoutList; this.childLayoutPairs.forEach(({ layout: doc }) => { @@ -194,7 +226,7 @@ export class CollectionGridView extends CollectionSubView() { undoBatch(() => this.setLayoutList(savedLayouts))(); } } - } + }; /** * @returns a list of `ContentFittingDocumentView`s inside wrapper divs. @@ -209,11 +241,12 @@ export class CollectionGridView extends CollectionSubView() { const dxf = () => this.lookupIndividualTransform(l); const width = () => (this.flexGrid ? l.w : this.defaultW) * this.colWidthPlusGap - this.margin; const height = () => (this.flexGrid ? l.h : this.defaultH) * this.rowHeightPlusGap - this.margin; - child && collector.push( - <div key={child.layout[Id]} className={"document-wrapper" + (this.flexGrid && this.props.isSelected() ? "" : " static")} > - {this.getDisplayDoc(child.layout, dxf, width, height)} - </div > - ); + child && + collector.push( + <div key={child.layout[Id]} className={'document-wrapper' + (this.flexGrid && this.props.isSelected() ? '' : ' static')}> + {this.getDisplayDoc(child.layout, dxf, width, height)} + </div> + ); }); } return collector; @@ -223,14 +256,19 @@ export class CollectionGridView extends CollectionSubView() { * @returns a list of `Layout` objects with attributes depending on whether the grid is flexible or static */ @computed get renderedLayoutList(): Layout[] { - return this.flexGrid ? - this.savedLayoutList.map(({ i, x, y, w, h }) => ({ - i, y, h, - x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases - w: Math.min(w, this.numCols), // reduces width if greater than numCols - static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout._lockedPosition, false) // checks if the lock position item has been selected in the context menu - })) : - this.savedLayoutList.map((layout, index) => { Object.assign(layout, this.unflexedPosition(index)); return layout; }); + return this.flexGrid + ? this.savedLayoutList.map(({ i, x, y, w, h }) => ({ + i, + y, + h, + x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases + w: Math.min(w, this.numCols), // reduces width if greater than numCols + static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout._lockedPosition, false), // checks if the lock position item has been selected in the context menu + })) + : this.savedLayoutList.map((layout, index) => { + Object.assign(layout, this.unflexedPosition(index)); + return layout; + }); } /** @@ -246,7 +284,7 @@ export class CollectionGridView extends CollectionSubView() { return true; } return false; - } + }; /** * Handles external drop of images/PDFs etc from outside Dash. @@ -255,7 +293,7 @@ export class CollectionGridView extends CollectionSubView() { onExternalDrop = async (e: React.DragEvent): Promise<void> => { this.dropLocation = this.screenToCell(e.clientX, e.clientY); super.onExternalDrop(e, {}); - } + }; /** * Handles the change in the value of the rowHeight slider. @@ -263,65 +301,83 @@ export class CollectionGridView extends CollectionSubView() { @action onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => { this._rowHeight = event.currentTarget.valueAsNumber; - } + }; /** * Handles the user clicking on the slider. */ @action onSliderDown = (e: React.PointerEvent) => { this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable - setupMoveUpEvents(this, e, returnFalse, action(() => { - undoBatch(() => this.props.Document.gridRowHeight = this._rowHeight)(); - this._rowHeight = undefined; - }), emptyFunction, false, false); + setupMoveUpEvents( + this, + e, + returnFalse, + action(() => { + undoBatch(() => (this.props.Document.gridRowHeight = this._rowHeight))(); + this._rowHeight = undefined; + }), + emptyFunction, + false, + false + ); e.stopPropagation(); - } + }; /** * Adds the display option to change the css display attribute of the `ContentFittingDocumentView`s */ onContextMenu = () => { const displayOptionsMenu: ContextMenuProps[] = []; - displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" }); - displayOptionsMenu.push({ description: "Toggle Vertical Centering", event: () => this.props.Document.centerY = !this.props.Document.centerY, icon: "copy" }); - ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" }); - } + displayOptionsMenu.push({ description: 'Toggle Content Display Style', event: () => (this.props.Document.display = this.props.Document.display ? undefined : 'contents'), icon: 'copy' }); + displayOptionsMenu.push({ description: 'Toggle Vertical Centering', event: () => (this.props.Document.centerY = !this.props.Document.centerY), icon: 'copy' }); + ContextMenu.Instance.addItem({ description: 'Display', subitems: displayOptionsMenu, icon: 'tv' }); + }; /** * Handles text document creation on double click. */ onPointerDown = (e: React.PointerEvent) => { if (this.props.isContentActive(true)) { - setupMoveUpEvents(this, e, returnFalse, returnFalse, + setupMoveUpEvents( + this, + e, + returnFalse, + returnFalse, (e: PointerEvent, doubleTap?: boolean) => { if (doubleTap && !e.button) { - undoBatch(action(() => { - const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 }); - FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); - this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); - }))(); + undoBatch( + action(() => { + const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 }); + FormattedTextBox.SelectOnLoad = text[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); + this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); + }) + )(); } }, - false); + false + ); if (this.props.isSelected(true)) e.stopPropagation(); } - } + }; render() { return ( - <div className="collectionGridView-contents" ref={this.createDashEventsTarget} - style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined }} + <div + className="collectionGridView-contents" + ref={this.createDashEventsTarget} + style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined }} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} - onDrop={this.onExternalDrop} - > - <div className="collectionGridView-gridContainer" ref={this._containerRef} - style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, "white") }} + onDrop={this.onExternalDrop}> + <div + className="collectionGridView-gridContainer" + ref={this._containerRef} + style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, 'white') }} onWheel={e => e.stopPropagation()} onScroll={action(e => { if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll; else this._scroll = e.currentTarget.scrollTop; - })} > + })}> <Grid width={this.props.PanelWidth()} nodeList={this.contents.length ? this.contents : null} @@ -332,15 +388,21 @@ export class CollectionGridView extends CollectionSubView() { setLayout={this.setLayout} transformScale={this.props.ScreenToLocalTransform().Scale} compactType={this.compaction} // determines whether nodes should remain in position, be bound to the top, or to the left - preventCollision={BoolCast(this.props.Document.gridPreventCollision)}// determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them + preventCollision={BoolCast(this.props.Document.gridPreventCollision)} // determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them margin={this.margin} /> - <input className="rowHeightSlider" type="range" + <input + className="rowHeightSlider" + type="range" style={{ width: this.props.PanelHeight() - 30 }} - min={1} value={this.rowHeight} max={this.props.PanelHeight() - 30} - onPointerDown={this.onSliderDown} onChange={this.onSliderChange} /> + min={1} + value={this.rowHeight} + max={this.props.PanelHeight() - 30} + onPointerDown={this.onSliderDown} + onChange={this.onSliderChange} + /> </div> - </div > + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 777ef464f..465dbfe6d 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,6 +1,6 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -11,11 +11,10 @@ import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionMulticolumnView.scss"; +import './CollectionMulticolumnView.scss'; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; - interface WidthSpecifier { magnitude: number; unit: string; @@ -27,8 +26,8 @@ interface LayoutData { } export const DimUnit = { - Pixel: "px", - Ratio: "*" + Pixel: 'px', + Ratio: '*', }; const resolvedUnits = Object.values(DimUnit); @@ -36,14 +35,13 @@ const resizerWidth = 8; @observer export class CollectionMulticolumnView extends CollectionSubView() { - /** * @returns the list of layout documents whose width unit is * *, denoting that it will be displayed with a ratio, not fixed pixel, value */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed @@ -65,10 +63,10 @@ export class CollectionMulticolumnView extends CollectionSubView() { let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout._dimUnit, "*"); + const unit = StrCast(pair.layout._dimUnit, '*'); const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === DimUnit.Ratio) && (starSum += magnitude); + unit === DimUnit.Ratio && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); } /** @@ -100,14 +98,13 @@ export class CollectionMulticolumnView extends CollectionSubView() { * This returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with higher priority) requested a fixed pixel width. - * + * * If the underlying resolvedLayoutInformation returns null * because we're waiting on promises to resolve, this value will be undefined as well. */ @computed private get totalFixedAllocation(): number | undefined { - return this.resolvedLayoutInformation?.widthSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); + return this.resolvedLayoutInformation?.widthSpecifiers.reduce((sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -115,7 +112,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. - * + * * If the underlying totalFixedAllocation returns undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -135,7 +132,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { * this accessor returns 1000 / (2 + 2 + 1), or 200px. * Elsewhere, this is then multiplied by each relative-width * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). - * + * * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -165,17 +162,17 @@ export class CollectionMulticolumnView extends CollectionSubView() { return 0; // we're still waiting on promises to resolve } let width = NumCast(layout._dimMagnitude, this.minimumDim); - if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { + if (StrCast(layout._dimUnit, '*') === DimUnit.Ratio) { width *= columnUnitLength; } return width; - } + }; /** * @returns the transform that will correctly place * the document decorations box, shifted to the right by * the sum of all the resolved column widths of the - * documents before the target. + * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { const columnUnitLength = this.columnUnitLength; @@ -185,12 +182,12 @@ export class CollectionMulticolumnView extends CollectionSubView() { let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - return this.props.ScreenToLocalTransform().translate(-offset / (this.props.scaling?.() || 1), 0); + return this.props.ScreenToLocalTransform().translate(-offset / (this.props.NativeDimScaling?.() || 1), 0); } offset += this.lookupPixels(candidate) + resizerWidth; } return Transform.Identity(); // type coersion, this case should never be hit - } + }; @undoBatch @action @@ -198,16 +195,17 @@ export class CollectionMulticolumnView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - })); + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + curInd = this.childDocs.indexOf(d); + }) + ); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.x < de.x && brect.x + brect.width > de.x) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { dropInd = curInd; - } - else if (child.className === "multiColumnResizer") { + } else if (child.className === 'multiColumnResizer') { dropInd = Math.floor(index / 2); } else { dropInd = Math.ceil(index / 2 + (de.x - brect.x > brect.width / 2 ? 0 : -1)); @@ -215,76 +213,80 @@ export class CollectionMulticolumnView extends CollectionSubView() { } }); if (super.onInternalDrop(e, de)) { - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d._dimUnit = "*"; - d._dimMagnitude = 1; - if (dropInd !== curInd || dropInd === -1) { - if (this.childDocs.includes(d)) { - if (dropInd > this.childDocs.indexOf(d)) dropInd--; + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + d._dimUnit = '*'; + d._dimMagnitude = 1; + if (dropInd !== curInd || dropInd === -1) { + if (this.childDocs.includes(d)) { + if (dropInd > this.childDocs.indexOf(d)) dropInd--; + } + Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); + Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); } - Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); - Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); - } - })); + }) + ); } } return false; - } - + }; onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { - if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); return true; } return this.props.addDocTab(doc, where); - } + }; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + isChildContentActive = () => + ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { - return <DocumentView - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - styleProvider={this.props.styleProvider} - docViewPath={this.props.docViewPath} - LayoutTemplate={this.props.childLayoutTemplate} - LayoutTemplateString={this.props.childLayoutString} - renderDepth={this.props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} - onClick={this.onChildClickHandler} - onDoubleClick={this.onChildDoubleClickHandler} - suppressSetHeight={true} - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} - hideResizeHandles={this.props.childHideResizeHandles?.()} - hideDecorationTitle={this.props.childHideDecorationTitle?.()} - fitContentsToBox={this.props.fitContentsToBox} - focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - dontRegisterView={this.props.dontRegisterView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - />; - } + return ( + <DocumentView + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + styleProvider={this.props.styleProvider} + docViewPath={this.props.docViewPath} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + suppressSetHeight={true} + ScreenToLocalTransform={dxf} + isContentActive={this.isChildContentActive} + isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + fitContentsToBox={this.props.fitContentsToBox} + focus={this.props.focus} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + dontRegisterView={this.props.dontRegisterView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + ); + }; /** * @returns the resolved list of rendered child documents, displayed - * at their resolved pixel widths, each separated by a resizer. + * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { @@ -293,22 +295,20 @@ export class CollectionMulticolumnView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; - const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)).scale((this.props.scaling?.() || 1)); + const dxf = () => + this.lookupIndividualTransform(layout) + .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .scale(this.props.NativeDimScaling?.() || 1); const width = () => this.lookupPixels(layout); const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div className={"document-wrapper"} - key={"wrapper" + i} - style={{ width: width() }} > + <div className={'document-wrapper'} key={'wrapper' + i} style={{ width: width() }}> {this.getDisplayDoc(layout, dxf, width, height)} - <WidthLabel - layout={layout} - collectionDoc={Document} - /> + <WidthLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar width={resizerWidth} - key={"resizer" + i} + key={'resizer' + i} styleProvider={this.props.styleProvider} isContentActive={this.props.isContentActive} select={this.props.select} @@ -324,16 +324,19 @@ export class CollectionMulticolumnView extends CollectionSubView() { render(): JSX.Element { return ( - <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget} + <div + className={'collectionMulticolumnView_contents'} + ref={this.createDashEventsTarget} style={{ width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, - marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), - marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) - }} > + marginLeft: NumCast(this.props.Document._xMargin), + marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), + marginBottom: NumCast(this.props.Document._yMargin), + }}> {this.contents} </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 08385bcb5..f8de4e5de 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -1,6 +1,6 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import * as React from "react"; +import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -11,7 +11,7 @@ import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; -import "./CollectionMultirowView.scss"; +import './CollectionMultirowView.scss'; import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; @@ -26,8 +26,8 @@ interface LayoutData { } export const DimUnit = { - Pixel: "px", - Ratio: "*" + Pixel: 'px', + Ratio: '*', }; const resolvedUnits = Object.values(DimUnit); @@ -35,14 +35,13 @@ const resizerHeight = 8; @observer export class CollectionMultirowView extends CollectionSubView() { - /** * @returns the list of layout documents whose width unit is * *, denoting that it will be displayed with a ratio, not fixed pixel, value */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed @@ -64,10 +63,10 @@ export class CollectionMultirowView extends CollectionSubView() { let starSum = 0; const heightSpecifiers: HeightSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout._dimUnit, "*"); + const unit = StrCast(pair.layout._dimUnit, '*'); const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === DimUnit.Ratio) && (starSum += magnitude); + unit === DimUnit.Ratio && (starSum += magnitude); heightSpecifiers.push({ magnitude, unit }); } /** @@ -99,14 +98,13 @@ export class CollectionMultirowView extends CollectionSubView() { * This returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with higher priority) requested a fixed pixel width. - * + * * If the underlying resolvedLayoutInformation returns null * because we're waiting on promises to resolve, this value will be undefined as well. */ @computed private get totalFixedAllocation(): number | undefined { - return this.resolvedLayoutInformation?.heightSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); + return this.resolvedLayoutInformation?.heightSpecifiers.reduce((sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -114,7 +112,7 @@ export class CollectionMultirowView extends CollectionSubView() { * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. - * + * * If the underlying totalFixedAllocation returns undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -134,7 +132,7 @@ export class CollectionMultirowView extends CollectionSubView() { * this accessor returns 1000 / (2 + 2 + 1), or 200px. * Elsewhere, this is then multiplied by each relative-width * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). - * + * * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined * because we're waiting indirectly on promises to resolve, this value will be undefined as well. */ @@ -164,17 +162,17 @@ export class CollectionMultirowView extends CollectionSubView() { return 0; // we're still waiting on promises to resolve } let height = NumCast(layout._dimMagnitude, this.minimumDim); - if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { + if (StrCast(layout._dimUnit, '*') === DimUnit.Ratio) { height *= rowUnitLength; } return height; - } + }; /** * @returns the transform that will correctly place * the document decorations box, shifted to the right by * the sum of all the resolved row widths of the - * documents before the target. + * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { const rowUnitLength = this.rowUnitLength; @@ -184,13 +182,12 @@ export class CollectionMultirowView extends CollectionSubView() { let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - return this.props.ScreenToLocalTransform().translate(0, -offset / (this.props.scaling?.() || 1)); + return this.props.ScreenToLocalTransform().translate(0, -offset / (this.props.NativeDimScaling?.() || 1)); } offset += this.lookupPixels(candidate) + resizerHeight; } return Transform.Identity(); // type coersion, this case should never be hit - } - + }; @undoBatch @action @@ -198,16 +195,17 @@ export class CollectionMultirowView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - })); + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + curInd = this.childDocs.indexOf(d); + }) + ); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { dropInd = curInd; - } - else if (child.className === "multiColumnResizer") { + } else if (child.className === 'multiColumnResizer') { dropInd = Math.floor(index / 2); } else { dropInd = Math.ceil(index / 2 + (de.y - brect.y > brect.height / 2 ? 0 : -1)); @@ -215,75 +213,79 @@ export class CollectionMultirowView extends CollectionSubView() { } }); if (super.onInternalDrop(e, de)) { - de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d._dimUnit = "*"; - d._dimMagnitude = 1; - if (dropInd !== curInd || dropInd === -1) { - if (this.childDocs.includes(d)) { - if (dropInd > this.childDocs.indexOf(d)) dropInd--; + de.complete.docDragData?.droppedDocuments.forEach( + action((d: Doc) => { + d._dimUnit = '*'; + d._dimMagnitude = 1; + if (dropInd !== curInd || dropInd === -1) { + if (this.childDocs.includes(d)) { + if (dropInd > this.childDocs.indexOf(d)) dropInd--; + } + Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); + Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); } - Doc.RemoveDocFromList(this.rootDoc, this.props.fieldKey, d); - Doc.AddDocToList(this.rootDoc, this.props.fieldKey, d, DocListCast(this.rootDoc[this.props.fieldKey])[dropInd], undefined, dropInd === -1); - } - })); + }) + ); } } return false; - } - + }; onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { - if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); return true; } return this.props.addDocTab(doc, where); - } + }; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + isChildContentActive = () => + ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { - return <DocumentView - Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} - styleProvider={this.props.styleProvider} - docViewPath={this.props.docViewPath} - LayoutTemplate={this.props.childLayoutTemplate} - LayoutTemplateString={this.props.childLayoutString} - renderDepth={this.props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} - onClick={this.onChildClickHandler} - onDoubleClick={this.onChildDoubleClickHandler} - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} - hideResizeHandles={this.props.childHideResizeHandles?.()} - hideDecorationTitle={this.props.childHideDecorationTitle?.()} - fitContentsToBox={this.props.fitContentsToBox} - focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - dontRegisterView={this.props.dontRegisterView} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - />; - } + return ( + <DocumentView + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + styleProvider={this.props.styleProvider} + docViewPath={this.props.docViewPath} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={dxf} + isContentActive={this.isChildContentActive} + isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + fitContentsToBox={this.props.fitContentsToBox} + focus={this.props.focus} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + dontRegisterView={this.props.dontRegisterView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + ); + }; /** * @returns the resolved list of rendered child documents, displayed - * at their resolved pixel widths, each separated by a resizer. + * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { @@ -292,13 +294,14 @@ export class CollectionMultirowView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; - const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)).scale((this.props.scaling?.() || 1)); + const dxf = () => + this.lookupIndividualTransform(layout) + .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .scale(this.props.NativeDimScaling?.() || 1); const height = () => this.lookupPixels(layout); const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div className={"document-wrapper"} - style={{ height: height() }} - key={"wrapper" + i} > + <div className={'document-wrapper'} style={{ height: height() }} key={'wrapper' + i}> {this.getDisplayDoc(layout, dxf, width, height)} <HeightLabel layout={layout} collectionDoc={Document} /> </div>, @@ -306,7 +309,7 @@ export class CollectionMultirowView extends CollectionSubView() { height={resizerHeight} styleProvider={this.props.styleProvider} isContentActive={this.props.isContentActive} - key={"resizer" + i} + key={'resizer' + i} columnUnitLength={this.getRowUnitLength} toTop={layout} toBottom={childLayoutPairs[i + 1]?.layout} @@ -319,16 +322,19 @@ export class CollectionMultirowView extends CollectionSubView() { render(): JSX.Element { return ( - <div className={"collectionMultirowView_contents"} + <div + className={'collectionMultirowView_contents'} style={{ width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, - marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), - marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) - }} ref={this.createDashEventsTarget}> + marginLeft: NumCast(this.props.Document._xMargin), + marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), + marginBottom: NumCast(this.props.Document._yMargin), + }} + ref={this.createDashEventsTarget}> {this.contents} </div> ); } - -}
\ No newline at end of file +} |
