diff options
| author | Hannah <hannah_chow@brown.edu> | 2020-02-11 06:34:19 -0500 |
|---|---|---|
| committer | Hannah <hannah_chow@brown.edu> | 2020-02-11 06:34:19 -0500 |
| commit | b60154479add51f826afaeff074cf43a6b416831 (patch) | |
| tree | 723f7f1356b1c3dcf5c409347b125d5a577b538f /src/client/views/collections | |
| parent | 74ca8ac01e32faa8adab54ddfd806fe46cda03ec (diff) | |
| parent | dc6453e27375a1b3d614a74b7fd1d83695f130d7 (diff) | |
merge
Diffstat (limited to 'src/client/views/collections')
31 files changed, 2017 insertions, 533 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss new file mode 100644 index 000000000..4815f1a59 --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -0,0 +1,40 @@ + +.collectionCarouselView-outer { + background: gray; + .collectionCarouselView-caption { + margin-left: 10%; + margin-right: 10%; + height: 50; + display: inline-block; + width: 80%; + } + .collectionCarouselView-image { + height: calc(100% - 50px); + display: inline-block; + width: 100%; + } +} +.carouselView-back { + position: absolute; + display: flex; + left: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +} +.carouselView-fwd { + position: absolute; + display: flex; + right: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx new file mode 100644 index 000000000..0933d5924 --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -0,0 +1,82 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observable, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../new_fields/Schema'; +import { NumCast, StrCast } from '../../../new_fields/Types'; +import { DragManager } from '../../util/DragManager'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import "./CollectionCarouselView.scss"; +import { CollectionSubView } from './CollectionSubView'; +import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { Doc } from '../../../new_fields/Doc'; +import { FormattedTextBox } from '../nodes/FormattedTextBox'; + + + + +type CarouselDocument = makeInterface<[typeof documentSchema,]>; +const CarouselDocument = makeInterface(documentSchema); + +@observer +export class CollectionCarouselView extends CollectionSubView(CarouselDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + private _dropDisposer?: DragManager.DragDropDisposer; + + componentWillUnmount() { + this._dropDisposer && this._dropDisposer(); + } + + componentDidMount() { + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + } + } + + advance = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length; + } + goback = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + + panelHeight = () => this.props.PanelHeight() - 50; + @computed get content() { + const index = NumCast(this.layoutDoc._itemIndex); + return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : + <div> + <div className="collectionCarouselView-image"> + <ContentFittingDocumentView {...this.props} + Document={this.childLayoutPairs[index].layout} + DataDocument={this.childLayoutPairs[index].data} + PanelHeight={this.panelHeight} + getTransform={this.props.ScreenToLocalTransform} /> + </div> + <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}> + <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox> + </div> + </div> + } + @computed get buttons() { + return <> + <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}> + <FontAwesomeIcon icon={faCaretLeft} size={"2x"} /> + </div> + <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}> + <FontAwesomeIcon icon={faCaretRight} size={"2x"} /> + </div> + </>; + } + render() { + return <div className="collectionCarouselView-outer"> + {this.content} + {this.buttons} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 151b84c50..82cb3bc88 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -34,6 +34,7 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { ComputedField } from '../../../new_fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; import { TraceMobx } from '../../../new_fields/util'; +import { Scripting } from '../../util/Scripting'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -131,7 +132,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public static CloseRightSplit(document: Doc): boolean { + public static CloseRightSplit(document: Opt<Doc>): boolean { if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; let retVal = false; @@ -139,14 +140,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { + ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) || + (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) { child.contentItems[0].remove(); instance.layoutChanged(document); return true; } else { Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) || + (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)))) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); return true; @@ -171,13 +174,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); } + @undoBatch + @action + public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean { + if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] + }; + + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + + let retVal = false; + if (instance._goldenLayout.root.contentItems[0].isRow) { + retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { + if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanle) { + child.contentItems[0].remove(); + child.addChild(newContentItem, undefined, true); + instance.layoutChanged(document); + return true; + } else { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { + child.contentItems[j].remove(); + child.addChild(newContentItem, undefined, true); + return true; + } + return false; + }); + } + return false; + }); + } + if (retVal) { + instance.stateChanged(); + } + return retVal; + } + // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split // @undoBatch @action - public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false, libraryPath?: Doc[]) { + public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; const newItemStackConfig = { @@ -202,16 +244,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp collayout.config.width = 50; newContentItem.config.width = 50; } - if (minimize) { - // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes - // newContentItem.config.width = 10; - // newContentItem.config.height = 10; - } newContentItem.callDownwards('_$init'); instance.layoutChanged(); return true; } + // + // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // + @undoBatch + @action + public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { + document.isDisplayPanel = true; + if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) { + CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath); + } + } + @undoBatch @action public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => { @@ -476,7 +525,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); + this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); } }); @@ -596,7 +645,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { if (curPres) { const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); Doc.GetProto(pinDoc).presentationTargetDoc = doc; - Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()'); + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title?.toString()'); const data = Cast(curPres.data, listSpec(Doc)); if (data) { data.push(pinDoc); @@ -636,19 +685,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get layoutDoc() { return this._document && Doc.Layout(this._document); } - panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; + panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; contentScaling = () => { if (this.layoutDoc!.type === DocumentType.PDF) { - if ((this.layoutDoc && this.layoutDoc.fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) { - return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth); + if ((this.layoutDoc && this.layoutDoc._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); } else { - return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight); + return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); } } const nativeH = this.nativeHeight(); @@ -674,7 +723,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { if (doc.dockingConfig) { return MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, dataDoc, undefined, libraryPath); + return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); } else { @@ -694,7 +743,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} - ruleProvider={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -717,10 +765,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%", + height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", width: this.widthpercent }}> {this.docView} </div >); } } +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); }); +Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); }); diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss new file mode 100644 index 000000000..eae9e0220 --- /dev/null +++ b/src/client/views/collections/CollectionLinearView.scss @@ -0,0 +1,75 @@ +@import "../globalCssVariables"; +@import "../_nodeModuleOverrides"; + +.collectionLinearView-outer{ + overflow: hidden; + height:100%; + .collectionLinearView { + display:flex; + height: 100%; + >label { + background: $dark-color; + color: $light-color; + display: inline-block; + border-radius: 18px; + font-size: 12.5px; + width: 18px; + height: 18px; + margin-top:auto; + margin-bottom:auto; + margin-right: 3px; + cursor: pointer; + transition: transform 0.2s; + } + + label p { + padding-left:5px; + } + + label:hover { + background: $main-accent; + transform: scale(1.15); + } + + >input { + display: none; + } + >input:not(:checked)~.collectionLinearView-content { + display: none; + } + + >input:checked~label { + transform: rotate(45deg); + transition: transform 0.5s; + cursor: pointer; + } + + .collectionLinearView-content { + display: flex; + opacity: 1; + position: relative; + margin-top: auto; + + .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { + position:relative; + margin:auto; + margin-left: 3px; + transform-origin: center 80%; + } + .collectionLinearView-docBtn-scalable:hover { + transform: scale(1.15); + } + + .collectionLinearView-round-button { + width: 18px; + height: 18px; + border-radius: 18px; + font-size: 15px; + } + + .collectionLinearView-round-button:hover { + transform: scale(1.15); + } + } + } +} diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx new file mode 100644 index 000000000..e613bf411 --- /dev/null +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -0,0 +1,134 @@ +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { makeInterface } from '../../../new_fields/Schema'; +import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { Transform } from '../../util/Transform'; +import "./CollectionLinearView.scss"; +import { CollectionViewType } from './CollectionView'; +import { CollectionSubView } from './CollectionSubView'; +import { DocumentView } from '../nodes/DocumentView'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { ScriptField } from '../../../new_fields/ScriptField'; + + +type LinearDocument = makeInterface<[typeof documentSchema,]>; +const LinearDocument = makeInterface(documentSchema); + +@observer +export class CollectionLinearView extends CollectionSubView(LinearDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; + private _dropDisposer?: DragManager.DragDropDisposer; + private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; + + componentWillUnmount() { + this._dropDisposer && this._dropDisposer(); + this._widthDisposer && this._widthDisposer(); + this._selectedDisposer && this._selectedDisposer(); + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + }); + } + + componentDidMount() { + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + { fireImmediately: true } + ); + + this._selectedDisposer = reaction( + () => NumCast(this.props.Document.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log); + } + }), + { fireImmediately: true } + ); + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + } + } + + public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + + dimension = () => NumCast(this.props.Document._height); // 2 * the padding + getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { + if (!ele.current) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + + render() { + const guid = Utils.GenerateGuid(); + const flexDir: any = StrCast(this.Document.flexDirection); + return <div className="collectionLinearView-outer"> + <div className="collectionLinearView" ref={this.createDashEventsTarget} > + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} + onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> + <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> + + <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}> + {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + const nested = pair.layout._viewType === CollectionViewType.Linear; + const dref = React.createRef<HTMLDivElement>(); + const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); + const deltaSize = nativeWidth * .15 / 2; + return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} + style={{ + width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, + height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + }} > + <DocumentView + Document={pair.layout} + DataDoc={pair.data} + LibraryPath={this.props.LibraryPath} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + removeDocument={this.props.removeDocument} + onClick={undefined} + ScreenToLocalTransform={this.getTransform(dref)} + ContentScaling={returnOne} + PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing + PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + </DocumentView> + </div>; + })} + </div> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 26e0cc35a..e84b3b0dd 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -137,7 +137,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr addDocument = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); - const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value }); + const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value }); newDoc[key] = this.getValue(this.props.heading); return this.props.parent.props.addDocument(newDoc); } @@ -259,7 +259,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @computed get contentLayout() { const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); const style = this.props.parent; const collapsed = this._collapsed; - const chromeStatus = this.props.parent.props.Document.chromeStatus; + const chromeStatus = this.props.parent.props.Document._chromeStatus; const newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionPivotView.scss new file mode 100644 index 000000000..505091e98 --- /dev/null +++ b/src/client/views/collections/CollectionPivotView.scss @@ -0,0 +1,88 @@ +.collectionPivotView { + display: flex; + flex-direction: row; + position: absolute; + height: 100%; + width: 100%; + + .collectionPivotView-flyout { + width: 400px; + height: 300px; + display: inline-block; + + .collectionPivotView-flyout-item { + background-color: lightgray; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + } + } + + .pivotKeyEntry { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; + pointer-events: all; + padding: 5px; + border: 1px solid black; + } + + .collectionPivotView-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + + .collectionPivotView-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: center; + + .collectionPivotView-button { + align-items: center; + display: flex; + width: 100%; + height: 100%; + + .collectionPivotView-span { + margin: auto; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + text-align: center; + } + } + + .collectionPivotView-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } + } + + .collectionPivotView-pivot { + display: inline-block; + width: calc(100% - 200px); + height: 100%; + } + + .collectionPivotView-dragger { + background-color: lightgray; + height: 40px; + width: 20px; + position: absolute; + border-radius: 10px; + top: 55%; + border: 1px black solid; + z-index: 2; + left: -10px; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx new file mode 100644 index 000000000..440b6856b --- /dev/null +++ b/src/client/views/collections/CollectionPivotView.tsx @@ -0,0 +1,148 @@ +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Set } from "typescript-collections"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; +import { Cast, StrCast } from "../../../new_fields/Types"; +import { Docs } from "../../documents/Documents"; +import { EditableView } from "../EditableView"; +import { anchorPoints, Flyout } from "../TemplateMenu"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import "./CollectionPivotView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import { CollectionTreeView } from "./CollectionTreeView"; +import React = require("react"); + +@observer +export class CollectionPivotView extends CollectionSubView(doc => doc) { + componentDidMount() { + this.props.Document._freeformLayoutEngine = "pivot"; + const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked; + if (!this.props.Document._facetCollection) { + const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true }); + facetCollection.target = this.props.Document; + this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]); + + const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)"; + const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); "; + facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name }); + this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name }); + this.props.Document._facetCollection = facetCollection; + this.props.Document._fitToBox = true; + } + } + bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth; + getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0); + + @computed get _allFacets() { + const facets = new Set<string>(); + this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); + return facets.toArray(); + } + + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const facetCollection = this.props.Document._facetCollection; + if (facetCollection instanceof Doc) { + const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + (facetCollection.data as List<Doc>).splice(found, 1); + const docFilter = Cast(this.props.Document._docFilter, listSpec("string")); + if (docFilter) { + let index: number; + while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { + docFilter.splice(index, 3); + } + } + } else { + const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true }); + const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; + const params = { layoutDoc: Doc.name, dataDoc: Doc.name, }; + newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables); + Doc.AddDocToList(facetCollection, "data", newFacet); + } + } + } + _canClick = false; + _facetWidthOnDown = 0; + @observable _facetWidth = 200; + onPointerDown = (e: React.PointerEvent) => { + this._canClick = true; + this._facetWidthOnDown = e.screenX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + + + @action + onPointerMove = (e: PointerEvent) => { + this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); + Math.abs(e.movementX) > 6 && (this._canClick = false); + } + @action + onPointerUp = (e: PointerEvent) => { + if (Math.abs(e.screenX - this._facetWidthOnDown) < 6 && this._canClick) { + this._facetWidth = this._facetWidth < 15 ? 200 : 0; + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null); + const flyout = ( + <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}> + {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> + <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} /> + <span className="checkmark" /> + {facet} + </label>)} + </div> + ); + return !facetCollection ? (null) : + <div className="collectionPivotView" style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> + <div className={"pivotKeyEntry"}> + <EditableView + contents={this.props.Document.pivotField} + GetValue={() => StrCast(this.props.Document.pivotField)} + SetValue={value => { + if (value && value.length) { + this.props.Document.pivotField = value; + return true; + } + return false; + }} + /> + </div> + <div className="collectionPivotView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} > + <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} /> + </div> + <div className="collectionPivotView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}> + <div className="collectionPivotView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> + <div className="collectionPivotView-button"> + <span className="collectionPivotView-span">Facet Filters</span> + <FontAwesomeIcon icon={faEdit} size={"lg"} /> + </div> + </Flyout> + </div> + <div className="collectionPivotView-tree" key="tree"> + <CollectionTreeView {...this.props} Document={facetCollection} /> + </div> + </div> + <div className="collectionPivotView-pivot" key="pivot" style={{ width: this.bodyPanelWidth() }}> + <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} /> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 79a34bc00..4eba5dc26 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; +import { List } from "lodash"; library.add(faExpand); @@ -82,10 +83,20 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } @action - onPointerDown = (e: React.PointerEvent): void => { + onPointerDown = async (e: React.PointerEvent): Promise<void> => { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); + let url: string; + if (url = StrCast(this.props.rowProps.row.href)) { + try { + new URL(url); + const temp = window.open(url)!; + temp.blur(); + window.focus(); + } catch { } + } + // this._isEditing = true; // this.props.setIsEditing(true); @@ -145,7 +156,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { DataDoc: this.props.rowProps.original, LibraryPath: [], fieldKey: this.props.rowProps.column.id as string, - ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, @@ -226,14 +236,14 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (value.startsWith(":=")) { return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); } - const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (!script.compiled) { return false; } return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); }} OnFillDown={async (value: string) => { - const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { DocListCast(this.props.Document[this.props.fieldKey]). forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run)); diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 0114342b9..92dc8780e 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -286,7 +286,6 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { } @undoBatch - @action onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); @@ -296,7 +295,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { this.onSelect(this._searchTerm); } else { - this._searchTerm = this._key; + this.setSearchTerm(this._key); } } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index bb706e528..fa8be5177 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -144,7 +144,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { LibraryPath={this.props.LibraryPath} childDocs={this.childDocs} renderDepth={this.props.renderDepth} - ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} PanelWidth={this.previewWidth} PanelHeight={this.previewHeight} getTransform={this.getPreviewTransform} @@ -157,8 +156,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} /> </div>; } @@ -480,7 +477,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch createRow = () => { - const newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 }); + const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }); this.props.addDocument(newDoc); } @@ -627,6 +624,19 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return Array.from(Object.keys(keys)); } + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.Document.textwrappedSchemaRows = new List<string>([]); + } else { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } + } + @action toggleTextWrapRow = (doc: Doc): void => { const textWrapped = this.textWrappedRows; @@ -645,7 +655,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const expanded = {}; //@ts-ignore expandedRowsList.forEach(row => expanded[row] = true); - console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( + const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable style={{ position: "relative" }} @@ -681,7 +691,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); + // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }) } } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index e1577cfee..843c743db 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -385,4 +385,4 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 83c90810e..7592712e4 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,7 +4,7 @@ import { CursorProperty } from "csstype"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import Switch from 'rc-switch'; -import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Doc, HeightSym, WidthSym, DataSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; @@ -22,7 +22,6 @@ import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewField import { CollectionSubView } from "./CollectionSubView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; -import { ScriptBox } from "../ScriptBox"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; import { TraceMobx } from "../../../new_fields/util"; import { CollectionViewType } from "./CollectionView"; @@ -40,35 +39,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @observable _scroll = 0; // used to force the document decoration to update when scrolling @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } - @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } - @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } - @computed get yMargin() { return Math.max(this.props.Document.showTitle ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); } - @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } + @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); } + @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); } + @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); } + @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } - childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } - children(docs: Doc[]) { this._docXfs.length = 0; return docs.map((d, i) => { - const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - const layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d; - const width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); - const height = () => this.getDocHeight(layoutDoc); + const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + const height = () => this.getDocHeight(d); const dref = React.createRef<HTMLDivElement>(); - const dxf = () => this.getDocTransform(layoutDoc, dref.current!); + const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)} + {this.getDisplayDoc(d, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)} </div>; }); } @@ -109,13 +104,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { componentDidMount() { super.componentDidMount(); this._heightDisposer = reaction(() => { - if (this.props.Document.autoHeight) { + if (this.props.Document._autoHeight) { const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); if (this.isStackingView) { const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { const r1 = Math.max(maxHght, (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { - const val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); + const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); return val; }, this.yMargin)); return r1; @@ -130,7 +125,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }, (hgt: number) => { const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; - doc && hgt > 0 && (Doc.Layout(doc).height = hgt); + doc && hgt > 0 && (Doc.Layout(doc)._height = hgt); }, { fireImmediately: true } ); @@ -153,11 +148,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDropAndGestureTarget(ele!); //so the whole grid is the drop target? - } - - overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {}; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @@ -170,9 +161,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { Document={doc} DataDocument={dataDoc} LibraryPath={this.props.LibraryPath} - showOverlays={this.overlays} renderDepth={this.props.renderDepth + 1} - ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} @@ -187,24 +176,22 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} - previewScript={undefined}> + pinToPres={this.props.pinToPres}> </ContentFittingDocumentView>; } getDocHeight(d?: Doc) { if (!d) return 0; const layoutDoc = Doc.Layout(d); - const nw = NumCast(layoutDoc.nativeWidth); - const nh = NumCast(layoutDoc.nativeHeight); + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) { + if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -250,7 +237,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; - const axis = this.Document.viewType === CollectionViewType.Masonry ? 0 : 1; + const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1; plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); @@ -296,7 +283,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } @@ -311,7 +298,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropAndGestureTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} />; } @@ -321,15 +308,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref); const outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]()); const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform(). - translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)); + const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2; + const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0); + return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling); } sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } @@ -344,7 +333,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropAndGestureTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} setDocHeight={this.setDocHeight} />; @@ -367,7 +356,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode"; } onContextMenu = (e: React.MouseEvent): void => { @@ -378,11 +367,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); - - const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } @@ -406,20 +390,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { <div className="collectionStackingMasonry-cont" > <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} ref={this.createRef} + style={{ + overflowY: this.props.active() ? "auto" : "hidden", + transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`, + height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`, + transformOrigin: "top" + }} onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={e => e.stopPropagation()} > + onWheel={e => this.props.active() && e.stopPropagation()} > {this.renderedSections} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.Document.chromeStatus !== 'disabled' ? <Switch + {this.props.Document._chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.Document.chromeStatus !== 'view-mode'} + defaultChecked={this.props.Document._chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 65c4b3195..c4680fc28 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -4,7 +4,7 @@ import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ScriptField } from "../../../new_fields/ScriptField"; import { NumCast, StrCast } from "../../../new_fields/Types"; @@ -18,6 +18,12 @@ import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; import { TraceMobx } from "../../../new_fields/util"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageField } from "../../../new_fields/URLField"; +import { ImageBox } from "../nodes/ImageBox"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { RichTextField } from "../../../new_fields/RichTextField"; library.add(faPalette); @@ -133,9 +139,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action addDocument = (value: string, shiftDown?: boolean) => { + if (!value) return false; this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); - const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, 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 heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; @@ -258,6 +265,57 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @observable _headingsHack: number = 1; + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + + const dataDoc = this.props.parent.props.DataDoc || this.props.parent.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 = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document)); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.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) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.props.addDocument(created); + } + }, icon: "compress-arrows-alt" + })); + layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + + ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.setDefaultItem("::", (name: string): void => { + Doc.GetProto(this.props.parent.props.Document)[name] = ""; + const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + this.props.parent.props.addDocument(created); + } + }); + const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); + ContextMenu.Instance.displayMenu(pt[0], pt[1]); + } + render() { TraceMobx(); const cols = this.props.cols(); @@ -293,7 +351,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <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. @@ -333,7 +391,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - const chromeStatus = this.props.parent.props.Document.chromeStatus; + const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> @@ -358,7 +416,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> + <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> } diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 105061f46..8c7e113b2 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -23,10 +23,6 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) { this.props.Document.staves = 5; } - @computed get fieldExtensionDoc() { - return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); - } - @computed get addStaffButton() { return <div onPointerDown={this.addStaff}>+</div>; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b5067ea9e..f125403f9 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; @@ -24,6 +24,7 @@ import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; +import { InteractionUtils } from "../../util/InteractionUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -39,7 +40,6 @@ export interface CollectionViewProps extends FieldViewProps { export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; - ruleProvider: Doc | undefined; children?: never | (() => JSX.Element[]) | React.ReactNode; isAnnotationOverlay?: boolean; annotationsKey: string; @@ -49,48 +49,57 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; - protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this.dropDisposer && this.dropDisposer(); - this.gestureDisposer && this.gestureDisposer(); + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this.dropDisposer ?.(); + this.gestureDisposer ?.(); + this.multiTouchDisposer ?.(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); + this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view - this.createDropAndGestureTarget(ele); + this.createDashEventsTarget(ele); } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - if (args[1] instanceof Doc) { - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent")); + this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc) ?.[Id]], + (args) => { + const childLayout = Cast(this.props.Document.childLayout, Doc); + if (childLayout instanceof Doc) { + this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layout_fromParent")); } - else if (!(args[1] instanceof Promise)) { - this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout")); + else if (!(childLayout instanceof Promise)) { + this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout")); } - }); + }, { fireImmediately: true }); } componentWillUnmount() { this._childLayoutDisposer && this._childLayoutDisposer(); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey]; + const { annotationsKey, fieldKey } = this.props; + if (annotationsKey) { + return this.dataDoc[fieldKey + "-" + annotationsKey]; + } + return this.dataDoc[fieldKey]; } - get childLayoutPairs() { - return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! })); + get childLayoutPairs(): { layout: Doc; data: Doc; }[] { + const { Document, DataDoc } = this.props; + const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); + return validPairs.map(({ data, layout }) => ({ data: data!, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { return Cast(this.dataField, listSpec(Doc)); @@ -98,7 +107,34 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get childDocs() { const docs = DocListCast(this.dataField); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []); + const clusters: { [key: string]: { [value: string]: string } } = {}; + for (let i = 0; i < docFilters.length; i += 3) { + const [key, value, modifiers] = docFilters.slice(i, i + 3); + const cluster = clusters[key]; + if (!cluster) { + const child: { [value: string]: string } = {}; + child[value] = modifiers; + clusters[key] = child; + } else { + cluster[value] = modifiers; + } + } + const filteredDocs = docFilters.length ? viewedDocs.filter(d => { + for (const key of Object.keys(clusters)) { + const cluster = clusters[key]; + const satisfiesFacet = Object.keys(cluster).some(inner => { + const modifier = cluster[inner]; + return (modifier === "x") !== Doc.matchFieldValue(d, key, inner); + }); + if (!satisfiesFacet) { + return false; + } + } + return true; + }) : viewedDocs; + return filteredDocs; } @action @@ -146,7 +182,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (docDragData && !docDragData.applyAsTemplate) { if (de.altKey && docDragData.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layoutFromParent")); + Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layout_fromParent")); e.stopPropagation(); return true; } @@ -157,7 +193,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const movedDocs = docDragData.draggedDocuments; added = movedDocs.reduce((added: boolean, d, i) => docDragData.droppedDocuments[i] !== d ? this.props.addDocument(docDragData.droppedDocuments[i]) : - docDragData.moveDocument?.(d, this.props.Document, this.props.addDocument) || added, false); + docDragData.moveDocument ?.(d, this.props.Document, this.props.addDocument) || added, false); } else { added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -203,7 +239,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text })); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); } return; } @@ -213,8 +249,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; if (img) { const split = img.split("src=\"")[1].split("\"")[0]; - console.log("creating image", split); - const doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); + const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 }); ImageUtils.ExtractExif(doc); this.props.addDocument(doc); return; @@ -229,7 +264,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text }); + const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text }); this.props.addDocument(htmlDoc); } return; @@ -237,12 +272,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } if (text && text.indexOf("www.youtube.com/watch") !== -1) { const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); + this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 })); return; } let matches: RegExpExecArray | null; if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { - const newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); + const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." }); const proto = newBox.proto!; const documentId = matches[2]; proto[GoogleRef] = documentId; @@ -289,13 +324,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { formData.append('file', file); const dropFileName = file ? file.name : "-empty-"; - promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => { - results.map(action(({ clientAccessPath }: any) => { - const full = { ...options, width: 300, title: dropFileName }; + promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { + results.map(action((result: any) => { + const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result; + const full = { ...options, _width: 300, title: dropFileName }; const pathname = Utils.prepend(clientAccessPath); Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); - doc && this.props.addDocument(doc); + if (doc) { + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + nativeWidth && (proto["data-nativeWidth"] = nativeWidth); + nativeHeight && (proto["data-nativeHeight"] = nativeHeight); + contentSize && (proto.contentSize = contentSize); + this.props.addDocument(doc); + } }); })); })); @@ -306,7 +348,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); } else { if (text && !text.includes("https://")) { - this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 })); + this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); } batch.end(); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2b13d87ee..a7733ab5f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,19 +1,22 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked, runInAction } from "mobx"; +import { action, computed, observable, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { emptyFunction, Utils, returnFalse, emptyPath } from '../../../Utils'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { emptyFunction, emptyPath, returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { makeTemplate } from '../../util/DropConverter'; +import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -21,13 +24,17 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import { ImageBox } from '../nodes/ImageBox'; import { KeyValueBox } from '../nodes/KeyValueBox'; +import { ScriptBox } from '../ScriptBox'; import { Templates } from '../Templates'; -import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { CollectionViewType } from './CollectionView'; +import { RichTextField } from '../../../new_fields/RichTextField'; +import { ObjectField } from '../../../new_fields/ObjectField'; export interface TreeViewProps { @@ -38,7 +45,6 @@ export interface TreeViewProps { prevSibling?: Doc; renderDepth: number; deleteDoc: (doc: Doc) => boolean; - ruleProvider: Doc | undefined; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; @@ -51,12 +57,13 @@ export interface TreeViewProps { outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; outerXf: () => { translateX: number, translateY: number }; - treeViewId: string; + treeViewId: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; hideHeaderFields: () => boolean; preventTreeViewOpen: boolean; renderedIds: string[]; + onCheckedClick?: ScriptField; } library.add(faTrashAlt); @@ -173,8 +180,8 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined; + const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; return this.props.addDocument(doc); })} @@ -205,7 +212,7 @@ class TreeView extends React.Component<TreeViewProps> { ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); } - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); @@ -232,8 +239,8 @@ class TreeView extends React.Component<TreeViewProps> { if (inside) { addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); } - const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); - return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId)) || de.complete.docDragData.userDropAction) ? + const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); + return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) : de.complete.docDragData.moveDocument ? movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) @@ -251,21 +258,21 @@ class TreeView extends React.Component<TreeViewProps> { } docWidth = () => { const layoutDoc = Doc.Layout(this.props.document); - const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { const layoutDoc = Doc.Layout(this.props.document); const bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, - NumCast(this.props.containingCollection.height)))) : - NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50; + return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.containingCollection._height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; })()); } @@ -286,7 +293,7 @@ class TreeView extends React.Component<TreeViewProps> { DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath); + [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick); } else { contentElement = <EditableView key="editableView" @@ -317,8 +324,6 @@ class TreeView extends React.Component<TreeViewProps> { return rows; } - noOverlays = (doc: Doc) => ({ title: "", caption: "" }); - @computed get renderContent() { const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { @@ -331,7 +336,7 @@ class TreeView extends React.Component<TreeViewProps> { this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath)} + [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> @@ -345,8 +350,6 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} - showOverlays={this.noOverlays} - ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} fitToBox={this.boundsOfCollectionDocument !== undefined} PanelWidth={this.docWidth} PanelHeight={this.docHeight} @@ -359,16 +362,32 @@ class TreeView extends React.Component<TreeViewProps> { active={this.props.active} whenActiveChanged={emptyFunction} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} /> + pinToPres={this.props.pinToPres} /> </div>; } } + @action + bulletClick = (e: React.MouseEvent) => { + if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) { + // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; + ScriptCast(this.props.onCheckedClick).script.run({ + this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, + heading: this.props.containingCollection.title, + checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeViewId, + }, console.log); + } else { + this.treeViewOpen = !this.treeViewOpen; + } + e.stopPropagation(); + } + @computed get renderBullet() { - return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> - {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />} + const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}> + {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } /** @@ -377,7 +396,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderTitle() { const reference = React.createRef<HTMLDivElement>(); - const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); const headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} @@ -427,7 +446,7 @@ class TreeView extends React.Component<TreeViewProps> { } public static GetChildElements( childDocs: Doc[], - treeViewId: string, + treeViewId: Doc, containingCollection: Doc, dataDoc: Doc | undefined, key: string, @@ -448,7 +467,8 @@ class TreeView extends React.Component<TreeViewProps> { hideHeaderFields: () => boolean, preventTreeViewOpen: boolean, renderedIds: string[], - libraryPath: Doc[] | undefined + libraryPath: Doc[] | undefined, + onCheckedClick: ScriptField | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -496,7 +516,7 @@ class TreeView extends React.Component<TreeViewProps> { const rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { - const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); if (!pair.layout || pair.data instanceof Promise) { return (null); } @@ -526,7 +546,7 @@ class TreeView extends React.Component<TreeViewProps> { }; const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { - const aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0); + const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return !(child instanceof Doc) ? (null) : <TreeView @@ -536,10 +556,10 @@ class TreeView extends React.Component<TreeViewProps> { containingCollection={containingCollection} prevSibling={docs[i]} treeViewId={treeViewId} - ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc} key={child[Id]} indentDocument={indent} outdentDocument={outdent} + onCheckedClick={onCheckedClick} renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} @@ -608,6 +628,49 @@ export class CollectionTreeView extends CollectionSubView(Document) { layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); } + ContextMenu.Instance.addItem({ + description: "Buxton Layout", icon: "eye", event: () => { + DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { + DocListCast(d.data).map((img, i) => { + const caption = (d.captions as any)[i]?.data; + if (caption instanceof ObjectField) { + Doc.GetProto(img).caption = ObjectField.MakeCopy(caption as ObjectField); + } + img._hideSidebar = true; + d.captions = undefined; + }); + }); + const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create; + const { Document } = this.props; + const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; + const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`; + + const textDoc = TextDocument("", { title: "details", _autoHeight: true }); + const detailedLayout = Docs.Create.StackingDocument([ + CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }), + textDoc, + ], { _chromeStatus: "disabled", title: "detailed layout stack" }); + textDoc.data = new RichTextField(detailedTemplate, "short_description year company"); + detailedLayout.isTemplateDoc = makeTemplate(detailedLayout); + + const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? + cardLayout.proto!.layout = ImageBox.LayoutString("hero"); + cardLayout.showTitle = "title"; + cardLayout.showTitleHover = "titlehover"; + + Document.childLayout = cardLayout; + Document.childDetailed = detailedLayout; + Document._viewType = CollectionViewType.Pivot; + Document.pivotField = "company"; + } + }); + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ + description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, + "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", treeViewContainer: Doc.name }) + }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); @@ -622,17 +685,17 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - const dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + const dropAction = StrCast(this.props.Document._dropAction) as dropActionType; const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" - style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} + style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - <EditableView + {(this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} display={"block"} maxHeight={72} @@ -641,21 +704,47 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.dataDoc, "title", value, false); - const layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined; + const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); - })} /> + })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { - TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields), - BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath) + BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick)) } </ul> </div > ); } -}
\ No newline at end of file +} + +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) { + const allCollectionDocs = DocListCast(dataDoc[dataKey]); + const facetValues = Array.from(allCollectionDocs.reduce((set, child) => + set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); + + const facetValueDocSet = facetValues.sort().map(facetValue => + Docs.Create.TextDocument("", { + title: facetValue.toString(), + treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", + { layoutDoc: Doc.name, facetHeader: "string", facetValue: "string" }, + { layoutDoc, facetHeader, facetValue }) + })); + return new List<Doc>(facetValueDocSet); +}); + +Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { + const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []); + for (let i = 0; i < docFilters.length; i += 3) { + const [header, value, state] = docFilters.slice(i, i + 3); + if (header === facetHeader && value === facetValue) { + return state; + } + } + return undefined; +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 1d399e26f..ab6ac0eaf 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,39 +1,42 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; +import Lightbox from 'react-image-lightbox-with-rotate'; +import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app +import { DateField } from '../../../new_fields/DateField'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { StrCast, BoolCast, Cast } from '../../../new_fields/Types'; +import { listSpec } from '../../../new_fields/Schema'; +import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types'; +import { ImageField } from '../../../new_fields/URLField'; +import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { ScriptBox } from '../ScriptBox'; +import { Touchable } from '../Touchable'; import { CollectionDockingView } from "./CollectionDockingView"; import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionCarouselView } from './CollectionCarouselView'; +import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; +import { CollectionPivotView } from './CollectionPivotView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; +import { CollectionStaffView } from './CollectionStaffView'; import { CollectionTreeView } from "./CollectionTreeView"; +import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { ImageUtils } from '../../util/Import & Export/ImageUtils'; -import { CollectionLinearView } from '../CollectionLinearView'; -import { CollectionStaffView } from './CollectionStaffView'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ImageField } from '../../../new_fields/URLField'; -import { DocListCast } from '../../../new_fields/Doc'; -import Lightbox from 'react-image-lightbox-with-rotate'; -import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app export const COLLECTION_BORDER_WIDTH = 2; -import { DateField } from '../../../new_fields/DateField'; -import { Doc, } from '../../../new_fields/Doc'; -import { listSpec } from '../../../new_fields/Schema'; -import { DocumentManager } from '../../util/DocumentManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import './CollectionView.scss'; -import { FieldViewProps, FieldView } from '../nodes/FieldView'; -import { Touchable } from '../Touchable'; -import { TraceMobx } from '../../../new_fields/util'; -import { Utils } from '../../../Utils'; const path = require('path'); library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -45,9 +48,12 @@ export enum CollectionViewType { Tree, Stacking, Masonry, + Multicolumn, Pivot, + Carousel, Linear, - Staff + Staff, + Timeline } export namespace CollectionViewType { @@ -59,8 +65,10 @@ export namespace CollectionViewType { ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], + ["multicolumn", CollectionViewType.Multicolumn], ["pivot", CollectionViewType.Pivot], - ["linear", CollectionViewType.Linear] + ["carousel", CollectionViewType.Carousel], + ["linear", CollectionViewType.Linear], ]); export const valueOf = (value: string) => stringMapping.get(value.toLowerCase()); @@ -87,7 +95,7 @@ export class CollectionView extends Touchable<FieldViewProps> { public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } get collectionViewType(): CollectionViewType | undefined { - const viewField = Cast(this.props.Document.viewType, "number"); + const viewField = NumCast(this.props.Document._viewType); if (CollectionView._safeMode) { if (viewField === CollectionViewType.Freeform) { return CollectionViewType.Tree; @@ -96,15 +104,15 @@ export class CollectionView extends Touchable<FieldViewProps> { return CollectionViewType.Freeform; } } - return viewField === undefined ? CollectionViewType.Invalid : viewField; + return viewField; } componentDidMount = () => { - this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), + this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus), () => { // chrome status is one of disabled, collapsed, or visible. this determines initial state from document // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar. - const chromeStatus = this.props.Document.chromeStatus; + const chromeStatus = this.props.Document._chromeStatus; if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { runInAction(() => this._collapsed = true); } @@ -114,7 +122,7 @@ export class CollectionView extends Touchable<FieldViewProps> { componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer(); // bcz: Argh? What's the height of the collection chromes?? - chromeHeight = () => (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0); active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; @@ -124,8 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps> { addDocument(doc: Doc): boolean { const targetDataDoc = Doc.GetProto(this.props.Document); Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); - const extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field - extension && (extension.lastModified = new DateField(new Date(Date.now()))); + targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); Doc.GetProto(doc).lastOpened = new DateField; return true; } @@ -172,24 +179,26 @@ export class CollectionView extends Touchable<FieldViewProps> { case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } + case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); } case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } - case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); } + case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); } case CollectionViewType.Freeform: - default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } + default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } } @action private collapse = (value: boolean) => { this._collapsed = value; - this.props.Document.chromeStatus = value ? "collapsed" : "enabled"; + this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - const chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : + const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; return [chrome, this.SubViewHelper(type, renderProps)]; } @@ -199,23 +208,25 @@ export class CollectionView extends Touchable<FieldViewProps> { if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 const existingVm = ContextMenu.Instance.findByDescription("View Modes..."); const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); + subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" }); if (CollectionView._safeMode) { - ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } - subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); - subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" }); - subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); + subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" }); + subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" }); + subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); subItems.push({ description: "Stacking (AutoHeight)", event: () => { - this.props.Document.viewType = CollectionViewType.Stacking; - this.props.Document.autoHeight = true; + this.props.Document._viewType = CollectionViewType.Stacking; + this.props.Document._autoHeight = true; }, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" }); - subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); - subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" }); - switch (this.props.Document.viewType) { + subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" }); + subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" }); + subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" }); + subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" }); + subItems.push({ description: "Pivot", event: () => this.props.Document._viewType = CollectionViewType.Pivot, icon: "columns" }); + switch (this.props.Document._viewType) { case CollectionViewType.Freeform: { subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); break; @@ -227,12 +238,23 @@ export class CollectionView extends Touchable<FieldViewProps> { const existing = ContextMenu.Instance.findByDescription("Layout..."); const layoutItems = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + if (this.props.Document.childLayout instanceof Doc) { + layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" }); + } + if (this.props.Document.childDetailed instanceof Doc) { + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" }); + } !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); const more = ContextMenu.Instance.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 64411b5fe..414bbfc0b 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -9,8 +9,7 @@ background: lightgrey; .collectionViewChrome { - display: grid; - grid-template-columns: 1fr auto; + display: flex; padding-bottom: 10px; border-bottom: .5px solid rgb(180, 180, 180); overflow: hidden; @@ -20,7 +19,7 @@ .collectionViewBaseChrome-viewPicker { font-size: 75%; - text-transform: uppercase; + //text-transform: uppercase; letter-spacing: 2px; background: rgb(238, 238, 238); color: grey; @@ -34,6 +33,26 @@ outline-color: black; } + .collectionViewBaseChrome-cmdPicker { + margin-left: 3px; + margin-right: 0px; + background: rgb(238, 238, 238); + border: none; + color: grey; + } + .commandEntry-outerDiv { + pointer-events: all; + background-color: gray; + display: flex; + flex-direction: row; + .commandEntry-drop { + color:white; + width:25px; + margin-top: auto; + margin-bottom: auto; + } + } + .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; @@ -53,6 +72,18 @@ .collectionViewBaseChrome-viewSpecs { margin-left: 10px; display: grid; + + .collectionViewBaseChrome-filterIcon { + position: relative; + display: flex; + margin: auto; + background: gray; + color: white; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + } .collectionViewBaseChrome-viewSpecsInput { padding: 12px 10px 11px 10px; @@ -240,14 +271,13 @@ .commandEntry-outerDiv { display: flex; flex-direction: column; - width: 165px; height: 40px; } .commandEntry-inputArea { display:flex; flex-direction: row; width: 150px; - margin: auto 0 auto auto; + margin: auto auto auto auto; } .react-autosuggest__container { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 01dc21f95..0811654bf 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -39,31 +39,39 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { - title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"], + title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; + _narrativeCommand = { + title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"], + initialize: emptyFunction, + immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) + }; _contentCommand = { - // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... - title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], + title: "=> content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d))) }; _viewCommand = { - title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], - immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; }, - initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; } + title: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], + initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document._panX; button.restoredPanY = this.props.CollectionView.props.Document._panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }, + immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; }, }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; + _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; + _schema_commands = [this._templateCommand, this._narrativeCommand]; _tree_commands = []; private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Tree: return this._tree_commands; + case CollectionViewType.Schema: return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; + case CollectionViewType.Pivot: return this._freeform_commands; + case CollectionViewType.Carousel: return this._freeform_commands; } return []; } @@ -126,7 +134,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro runInAction(() => { this.addKeyRestrictions(fields); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - const chromeStatus = this.props.CollectionView.props.Document.chromeStatus; + const chromeStatus = this.props.CollectionView.props.Document._chromeStatus; if (chromeStatus) { if (chromeStatus === "disabled") { throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); @@ -143,24 +151,35 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @undoBatch viewChanged = (e: React.ChangeEvent) => { //@ts-ignore - this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value); + this.props.CollectionView.props.Document._viewType = parseInt(e.target.selectedOptions[0].value); + } + + commandChanged = (e: React.ChangeEvent) => { + //@ts-ignore + runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } @action openViewSpecs = (e: React.SyntheticEvent) => { - this._viewSpecsOpen = true; + if (this._viewSpecsOpen) this.closeViewSpecs(); + else { + this._viewSpecsOpen = true; - //@ts-ignore - if (!e.target.classList[0].startsWith("qs")) { - this.closeDatePicker(); - } + //@ts-ignore + if (!e.target ?.classList[0] ?.startsWith("qs")) { + this.closeDatePicker(); + } - e.stopPropagation(); - document.removeEventListener("pointerdown", this.closeViewSpecs); - document.addEventListener("pointerdown", this.closeViewSpecs); + e.stopPropagation(); + document.removeEventListener("pointerdown", this.closeViewSpecs); + document.addEventListener("pointerdown", this.closeViewSpecs); + } } - @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); }; + @action closeViewSpecs = () => { + this._viewSpecsOpen = false; + document.removeEventListener("pointerdown", this.closeViewSpecs); + }; @action openDatePicker = (e: React.PointerEvent) => { @@ -231,10 +250,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action toggleCollapse = () => { - console.log("toggle collapse"); - this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled"; + this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { - this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled"); + this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); } } @@ -262,7 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private pivotKeyDisplay = this.pivotKey; getPivotInput = () => { - if (StrCast(this.document.freeformLayoutEngine) !== "pivot") { + if (StrCast(this.document._freeformLayoutEngine) !== "pivot") { return (null); } return (<input className="collectionViewBaseChrome-viewSpecsInput" @@ -297,7 +315,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData ?.draggedDocuments || [])); e.stopPropagation(); } return true; @@ -373,7 +391,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } render() { - const collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; + const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; return ( <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}> <div className="collectionViewChrome"> @@ -392,23 +410,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro className="collectionViewBaseChrome-viewPicker" onPointerDown={stopPropagation} onChange={this.viewChanged} - value={NumCast(this.props.CollectionView.props.Document.viewType)}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option> + value={NumCast(this.props.CollectionView.props.Document._viewType)}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">schema</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Multicolumn</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Pivot</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Carousel</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Linear</option> </select> - <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> - <input className="collectionViewBaseChrome-viewSpecsInput" - placeholder="FILTER" - value={this.filterValue ? this.filterValue.script.originalScript === "return true" ? "" : this.filterValue.script.originalScript : ""} - onChange={(e) => { }} - onPointerDown={this.openViewSpecs} - id="viewSpecsInput" /> - {this.getPivotInput()} + <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> + <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} > + <FontAwesomeIcon icon="filter" size="2x" /> + </div> <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} style={{ @@ -449,17 +465,20 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro </div> </div> <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > - <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-inputArea" onPointerDown={this.autoSuggestDown} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={this.suggestions} - alwaysRenderSuggestions={true} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={this.onSuggestionFetch} - onSuggestionsClearRequested={this.onSuggestionClear} - ref={this._autosuggestRef} /> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> </div> + <select + className="collectionViewBaseChrome-cmdPicker" + onPointerDown={stopPropagation} + onChange={this.commandChanged} + value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option> + {this._buttonizableCommands.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> </div> </div> </div> @@ -600,15 +619,6 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh return ( <div className="collectionSchemaViewChrome-cont"> <div className="collectionSchemaViewChrome-toggle"> - <div className="collectionSchemaViewChrome-label">Wrap Text: </div> - <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}> - <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}> - {textWrapped ? "on" : "off"} - </div> - </div> - </div> - - <div className="collectionSchemaViewChrome-toggle"> <div className="collectionSchemaViewChrome-label">Show Preview: </div> <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index d293bb5ca..a266861bd 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -35,8 +35,6 @@ pointer-events: all; position: relative; display: inline-block; - padding-left: 5px; - padding-right: 5px; } .parentDocumentSelector-metadata { pointer-events: auto; diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 24aa6ddfa..115f8d633 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -6,7 +6,7 @@ import { observable, action, runInAction } from "mobx"; import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast, StrCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; @@ -21,7 +21,13 @@ export const Flyout = higflyout.default; library.add(faEdit); -type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +type SelectorProps = { + Document: Doc, + Views: DocumentView[], + Stack?: any, + addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void +}; + @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -49,31 +55,22 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { getOnClick({ col, target }: { col: Doc, target: Doc }) { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / 2; - col.panX = newPanX; - col.panY = newPanY; + if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(target.x) + NumCast(target._width) / 2; + const newPanY = NumCast(target.y) + NumCast(target._height) / 2; + col._panX = newPanX; + col._panY = newPanY; } this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } - get metadataMenu() { - return <div className="parentDocumentSelector-metadata"> - <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => this.props.Views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} - <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> - </Flyout> - </div>; - } render() { return <div > - <div key="metadata">Metadata: {this.metadataMenu}</div> <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} </div>; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 012115b1f..be1317b25 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -7,8 +7,9 @@ import { OverlayView, OverlayElementOptions } from "../../OverlayView"; import { emptyFunction } from "../../../../Utils"; import React = require("react"); import { ObservableMap, runInAction } from "mobx"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { DateField } from "../../../../new_fields/DateField"; +import { Id, ToString } from "../../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../../new_fields/ObjectField"; +import { RefField } from "../../../../new_fields/RefField"; interface PivotData { type: string; @@ -35,21 +36,19 @@ export interface ViewDefResult { } function toLabel(target: FieldResult<Field>) { - if (target instanceof DateField) { - const date = DateCast(target).date; - if (date) { - return `${date.toDateString()} ${date.toTimeString()}`; - } + if (target instanceof ObjectField || target instanceof RefField) { + return target[ToString](); } return String(target); } -export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { +export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) { const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); + const pivotFieldKey = toLabel(pivotDoc.pivotField); for (const doc of childDocs) { - const val = doc[StrCast(pivotDoc.pivotField, "title")]; + const val = Field.toString(doc[pivotFieldKey] as Field); if (val) { !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []); pivotColumnGroups.get(val)!.push(doc); @@ -57,9 +56,10 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo } const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity); - const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); + let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); const docMap = new Map<Doc, ViewDefBounds>(); const groupNames: PivotData[] = []; + numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols); const expander = 1.05; const gap = .15; @@ -73,26 +73,26 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo x, y: pivotAxisWidth + 50, width: pivotAxisWidth * expander * numCols, - height: 100, + height: NumCast(pivotDoc.pivotFontSize, 10), fontSize: NumCast(pivotDoc.pivotFontSize, 10) }); for (const doc of val) { const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; - let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; if (hgt > pivotAxisWidth) { hgt = pivotAxisWidth; - wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; } docMap.set(doc, { - x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2, + x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0), y: -y, width: wid, height: hgt }); xCount++; if (xCount >= numCols) { - xCount = (pivotAxisWidth - wid) / 2; + xCount = 0; y += pivotAxisWidth * expander; } } @@ -104,8 +104,8 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo x: NumCast(pair.layout.x), y: NumCast(pair.layout.y), z: NumCast(pair.layout.z), - width: NumCast(pair.layout.width), - height: NumCast(pair.layout.height) + width: NumCast(pair.layout._width), + height: NumCast(pair.layout._height) }; const pos = docMap.get(pair.layout) || defaultPosition; const data = poolData.get(pair.layout[Id]); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 178a5bcdc..b8fbaef5c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -54,8 +54,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); - let m = targetBhyperlink.getBoundingClientRect(); - let mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetBhyperlink.getBoundingClientRect(); + const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; }, 0); @@ -66,8 +66,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); - let m = targetAhyperlink.getBoundingClientRect(); - let mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetAhyperlink.getBoundingClientRect(); + const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; }, 0); @@ -93,8 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - let aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - let bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); return !aActive && !bActive ? (null) : <line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4d216cb2a..d988ff241 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,13 +3,13 @@ import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkTool, InkField, InkData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, DateCast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; +import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; @@ -27,7 +27,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; @@ -41,21 +41,23 @@ import React = require("react"); import { computedFn } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { LinkManager } from "../../../util/LinkManager"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import Palette from "../../Palette"; +import { RichTextField } from "../../../../new_fields/RichTextField"; +import { List } from "../../../../new_fields/List"; +import { DocumentViewProps } from "../../nodes/DocumentView"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ - panX: "number", - panY: "number", + _panX: "number", + _panY: "number", scale: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", - isRuleProvider: "boolean", fitToBox: "boolean", + xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", @@ -71,25 +73,28 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; + private _inkToTextStartX: number | undefined; + private _inkToTextStartY: number | undefined; + private _wordPalette: Map<string, string> = new Map<string, string>(); private _clusterDistance: number = 75; private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; private _layoutPoolData = new ObservableMap<string, any>(); - public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title ?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; - @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } + @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } - @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } - @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); } + @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); } + @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private easing = () => this.props.Document.panTransformType === "Ease"; - private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0; - private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0; + private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; + private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ? Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : this.Document.scale || 1) @@ -101,14 +106,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - const maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); - let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0; - if (heading === 0) { - const sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 : - DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0); - heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading); - } - !this.Document.isRuleProvider && (newBox.heading = heading); this.addDocument(newBox); } private addDocument = (newBox: Doc) => { @@ -153,18 +150,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const layoutDoc = Doc.Layout(d); d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; - if (!NumCast(layoutDoc.width)) { - layoutDoc.width = 300; + if (!NumCast(layoutDoc._width)) { + layoutDoc._width = 300; } - if (!NumCast(layoutDoc.height)) { - const nw = NumCast(layoutDoc.nativeWidth); - const nh = NumCast(layoutDoc.nativeHeight); - layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300; + if (!NumCast(layoutDoc._height)) { + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; } this.bringToFront(d); })); - de.complete.docDragData.droppedDocuments.length === 1 && this.updateCluster(de.complete.docDragData.droppedDocuments[0]); + (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); } } else if (de.complete.annoDragData) { @@ -189,8 +186,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance; const cy = NumCast(cd.y) - this._clusterDistance; - const cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance; - const ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance; + const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance; + const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance; return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? NumCast(cd.cluster) : cluster; }, -1); @@ -222,6 +219,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } + @action + updateClusterDocs(docs: Doc[]) { + const childLayouts = this.childLayoutPairs.map(pair => pair.layout); + if (this.props.Document.useClusters) { + const docFirst = docs[0]; + docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); + const preferredInd = NumCast(docFirst.cluster); + docs.map(doc => doc.cluster = -1); + docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { + if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + docFirst.cluster = i; + } + }))); + if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + docFirst.cluster = preferredInd; + } + this._clusterSets.map((set, i) => { + if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + docFirst.cluster = i; + } + }); + if (docFirst.cluster === -1) { + docs.map(doc => { + doc.cluster = this._clusterSets.length; + this._clusterSets.push([doc]); + }); + } else { + for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); + docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); + } + childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); + childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster ?.toString()); + } + } + @undoBatch @action updateCluster(doc: Doc) { @@ -275,11 +307,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { return; } this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; - if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -324,30 +356,32 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - handle1PointerDown = (e: React.TouchEvent) => { - const pt = e.targetTouches.item(0); - if (pt) { - this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; - if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { - // e.stopPropagation(); - // e.preventDefault(); - // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); - // this._points.push({ X: point[0], Y: point[1] }); - // } - if (InkingControl.Instance.selectedTool === InkTool.None) { - this._lastX = pt.pageX; - this._lastY = pt.pageY; - e.stopPropagation(); - e.preventDefault(); - } - else { - e.stopPropagation(); - e.preventDefault(); + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble) { + // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt = me.changedTouches[0]; + if (pt) { + this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } + if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = pt.pageX; + this._lastY = pt.pageY; + e.preventDefault(); + e.stopPropagation(); + } + else { + e.preventDefault(); + } } } } @@ -359,7 +393,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height }); + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -381,10 +415,80 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } return pass; }); - this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 })); + this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); sel.forEach(d => this.props.removeDocument(d)); + e.stopPropagation(); break; - + case GestureUtils.Gestures.StartBracket: + const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); + this._inkToTextStartX = start[0]; + this._inkToTextStartY = start[1]; + console.log("start"); + break; + case GestureUtils.Gestures.EndBracket: + console.log("end"); + if (this._inkToTextStartX && this._inkToTextStartY) { + const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); + const setDocs = this.getActiveDocuments().filter(s => s.proto ?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField) ?.Text as string; + }); + if (sets.length && sets[0]) { + this._wordPalette.clear(); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + this._wordPalette.set(word, colors[i]); + }); + }); + } + const inks = this.getActiveDocuments().filter(doc => { + if (doc.type === "ink") { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); + return pass; + } + return false; + }); + const inkFields = inks.map(i => Cast(i.data, InkField)); + CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => { + const wordResults = results.filter((r: any) => r.category === "inkWord"); + for (const word of wordResults) { + const indices: number[] = word.strokeIds; + indices.forEach(i => { + const otherInks: Doc[] = []; + indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); + inks[i].relatedInks = new List<Doc>(otherInks); + const uniqueColors: string[] = []; + Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); + inks[i].alternativeColors = new List<string>(uniqueColors); + if (this._wordPalette.has(word.recognizedText.toLowerCase())) { + inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); + } + else { + for (const alt of word.alternates) { + if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { + inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); + break; + } + } + } + }); + } + }); + this._inkToTextStartX = end[0]; + } + break; + case GestureUtils.Gestures.Text: + if (ge.text) { + const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); + this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); + e.stopPropagation(); + } } } @@ -394,8 +498,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -403,22 +507,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // I think it makes sense for the marquee menu to go away when panned. -syip2 MarqueeOptionsMenu.Instance && MarqueeOptionsMenu.Instance.fadeOut(true); - let x = this.Document.panX || 0; - let y = this.Document.panY || 0; - const docs = this.childLayoutPairs.map(pair => pair.layout); + let x = this.Document._panX || 0; + let y = this.Document._panY || 0; + const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay) { - PDFMenu.Instance && PDFMenu.Instance.fadeOut(true); - const minx = docs.length ? NumCast(docs[0].x) : 0; - const maxx = docs.length ? NumCast(docs[0].width) + minx : minx; - const miny = docs.length ? NumCast(docs[0].y) : 0; - const maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + if (!this.isAnnotationOverlay && docs.length) { + PDFMenu.Instance.fadeOut(true); + const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0; + const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0; + const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx; + const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny; const ranges = docs.filter(doc => doc).reduce((range, doc) => { - const layoutDoc = Doc.Layout(doc); - const x = NumCast(doc.x); - const xe = x + NumCast(layoutDoc.width); - const y = NumCast(doc.y); - const ye = y + NumCast(layoutDoc.height); + const x = this.childDataProvider(doc).x;//NumCast(doc.x); + const y = this.childDataProvider(doc).y;//NumCast(doc.y); + const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width); + const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.height); return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); @@ -464,10 +567,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - handle1PointerMove = (e: TouchEvent) => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // panning a workspace if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { @@ -481,17 +584,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.pan(pt); } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } - handle2PointersMove = (e: TouchEvent) => { + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // pinch zooming if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; + console.log(myTouches); if (this.prevPoints.size === 2) { const oldPoint1 = this.prevPoints.get(pt1.identifier); @@ -526,35 +630,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } @action - handle2PointersDown = (e: React.TouchEvent) => { + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (!e.nativeEvent.cancelBubble && this.props.active(true)) { - const pt1: React.Touch | null = e.targetTouches.item(0); - const pt2: React.Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; - - const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; - this._lastX = centerX; - this._lastY = centerY; - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - e.stopPropagation(); + // const pt1: React.Touch | null = e.targetTouches.item(0); + // const pt2: React.Touch | null = e.targetTouches.item(1); + // // if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + if (pt1 && pt2) { + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } } } cleanUpInteractions = () => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -593,8 +701,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); - this.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + this.Document._panX = this.isAnnotationOverlay ? newPanX : panX; + this.Document._panY = this.isAnnotationOverlay ? newPanY : panY; } } @@ -618,14 +726,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state - if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) { + if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; if (!init) { - state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY }; + state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY }; HistoryUtil.pushState(state); - } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) { - init.panX = this.Document.panX; - init.panY = this.Document.panY; + } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) { + init.panX = this.Document._panX; + init.panY = this.Document._panY; HistoryUtil.pushState(state); } } @@ -641,13 +749,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } else { const layoutdoc = Doc.Layout(doc); - const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2; + const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; + const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2; const newState = HistoryUtil.getState(); newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - const savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow Doc.BrushDoc(this.props.Document); @@ -657,8 +765,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { afterFocus && setTimeout(() => { if (afterFocus && afterFocus()) { - this.Document.panX = savedState.px; - this.Document.panY = savedState.py; + this.Document._panX = savedState.px; + this.Document._panY = savedState.py; this.Document.scale = savedState.s; this.Document.panTransformType = savedState.pt; } @@ -668,7 +776,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { - this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height)); + this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } zoomToScale = (scale: number) => { @@ -678,6 +786,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale = () => this.Document.scale || 1; @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -686,8 +795,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { Document: childLayout, LibraryPath: this.libraryPath, layoutKey: undefined, - ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves - onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them + //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them + onClick: this.onChildClickHandler, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], @@ -705,12 +814,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { - const result = this.Document.arrangeScript?.script.run(params, console.log); - if (result?.success) { + const result = this.Document.arrangeScript ?.script.run(params, console.log); + if (result ?.success) { return { ...result, transition: "transform 1s" }; } const layoutDoc = Doc.Layout(params.doc); - return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") }; + return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") }; } viewDefsToJSX = (views: any[]) => { @@ -728,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const fontSize = Cast(viewDef.fontSize, "number"); return [text, x, y, width, height].some(val => val === undefined) ? undefined : { - ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> + ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> {text} </div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } @@ -736,11 +845,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { return this._layoutPoolData.get(doc[Id]); }.bind(this)); + childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { + if (!doc) { + console.log(doc); + } + return this._layoutPoolData.get(doc[Id]); + }.bind(this)); doPivotLayout(poolData: ObservableMap<string, any>) { return computePivotLayout(poolData, this.props.Document, this.childDocs, - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } doFreeformLayout(poolData: ObservableMap<string, any>) { @@ -762,15 +876,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { get doLayoutComputation() { let computedElementData: { elements: ViewDefResult[] }; - switch (this.Document.freeformLayoutEngine) { + switch (this.Document._freeformLayoutEngine) { case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break; default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break; } this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair => computedElementData.elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider} - ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} - jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} + dataProvider={this.childDataProvider} + jitterRotation={NumCast(this.props.Document.jitterRotation)} + fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />, bounds: this.childDataProvider(pair.layout) })); @@ -797,12 +912,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { const docs = DocListCast(this.Document[this.props.fieldKey]); - const startX = this.Document.panX || 0; + const startX = this.Document._panX || 0; let x = startX; - let y = this.Document.panY || 0; + let y = this.Document._panY || 0; let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); + const width = Math.max(...docs.map(doc => NumCast(doc._width))); + const height = Math.max(...docs.map(doc => NumCast(doc._height))); for (const doc of docs) { doc.x = x; doc.y = y; @@ -816,37 +931,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }, "arrange contents"); } - autoFormat = () => { - this.Document.isRuleProvider = !this.Document.isRuleProvider; - // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading. - this.Document.isRuleProvider && this.childLayoutPairs.map(pair => - // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template) - DocListCast(Doc.Layout(pair.layout).data).map(heading => { - const headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); - const headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout; - if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) { - Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor; - } - }) - ); - } - - analyzeStrokes = async () => { - const children = await DocListCastAsync(this.dataDoc.data); - if (!children) { - return; - } - const inkData: InkData[] = []; - for (const doc of children) { - const data = Cast(doc.data, InkField)?.inkData; - data && inkData.push(data); - } - if (!inkData.length) { - return; - } - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData); - } - private thumbIdentifier?: number; // @action @@ -893,16 +977,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onContextMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) { - layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); - } - layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: `${this.Document.LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document.LODdisable = !this.Document.LODdisable, icon: "table" }); - layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" }); layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); - layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); + // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); layoutItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => { @@ -936,7 +1016,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { description: "Add Note ...", subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ description: (i + 1) + ": " + StrCast(note.title), - event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })), + event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })), icon: "eye" })) as ContextMenuProps[], icon: "eye" @@ -957,7 +1037,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { children = () => { const eles: JSX.Element[] = []; - this.extensionDoc && (eles.push(...this.childViews())); + eles.push(...this.childViews()); // this._palette && (eles.push(this._palette)); // this.currentStroke && (eles.push(this.currentStroke)); eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); @@ -965,11 +1045,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @computed get placeholder() { return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title}</span> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title ?.toString()}</span> </div>; } @computed get marqueeView() { - return <MarqueeView {...this.props} extensionDoc={this.extensionDoc!} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} + return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> @@ -977,6 +1057,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { </CollectionFreeFormViewPannableContents> </MarqueeView>; } + + @computed get contentScaling() { + if (this.props.annotationsKey) return 0; + const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; + const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; + return wscale < hscale ? wscale : hscale; + } render() { TraceMobx(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) @@ -986,12 +1073,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey. // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document - if (!this.extensionDoc) return (null); // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale; - return <div className={"collectionfreeformview-container"} ref={this.createDropAndGestureTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, - style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> - {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? + return <div className={"collectionfreeformview-container"} + ref={this.createDashEventsTarget} + onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} + style={{ + pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + transform: this.contentScaling ? `scale(${this.contentScaling})` : "", + transformOrigin: this.contentScaling ? "left top" : "", + width: this.contentScaling ? `${100 / this.contentScaling}%` : "", + height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + }}> + {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> </div>; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 32e39d25e..db4b674b5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -11,6 +11,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; @@ -25,21 +26,31 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { <button className="antimodeMenu-button" title="Create a Collection" + key="group" onPointerDown={this.createCollection}> <FontAwesomeIcon icon="object-group" size="lg" /> </button>, <button className="antimodeMenu-button" title="Summarize Documents" + key="summarize" onPointerDown={this.summarize}> <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> </button>, <button className="antimodeMenu-button" title="Delete Documents" + key="delete" onPointerDown={this.delete}> <FontAwesomeIcon icon="trash-alt" size="lg" /> </button>, + <button + className="antimodeMenu-button" + title="Change to Text" + key="inkToText" + onPointerDown={this.inkToText}> + <FontAwesomeIcon icon="font" size="lg" /> + </button>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 523edb918..8591144c0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,12 +1,12 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc"; import { InkField } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { listSpec } from "../../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; import { ComputedField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; import { Docs } from "../../../documents/Documents"; @@ -19,6 +19,8 @@ import "./MarqueeView.scss"; import React = require("react"); import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { SubCollectionViewProps } from "../CollectionSubView"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { RichTextField } from "../../../../new_fields/RichTextField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -29,7 +31,6 @@ interface MarqueeViewProps { removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - extensionDoc: Doc; isAnnotationOverlay?: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -85,7 +86,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } ns.map(line => { const indent = line.search(/\S|$/); - const newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, title: line }); this.props.addDocument(newBox); y += 40 * this.props.getTransform().Scale; }); @@ -95,17 +96,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque navigator.clipboard.readText().then(text => { const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } }); } else if (!e.ctrlKey) { this.props.addLiveTextDocument( - Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" })); + Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" })); } else if (e.keyCode > 48 && e.keyCode <= 57) { const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); - const text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }); + const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); text.layout = notes[(e.keyCode - 49) % notes.length]; this.props.addLiveTextDocument(text); } @@ -128,7 +129,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque let groupAttr: string | number = ""; const rowProto = new Doc(); rowProto.title = rowProto.Id; - rowProto.width = 200; + rowProto._width = 200; rowProto.isPrototype = true; for (let i = 1; i < ns.length - 1; i++) { const values = ns[i].split("\t"); @@ -144,10 +145,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } docDataProto.title = i.toString(); const doc = Doc.MakeDelegate(docDataProto); - doc.width = 200; + doc._width = 200; docList.push(doc); } - const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 }); this.props.addDocument(newCol); } @@ -205,6 +206,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; + MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight; MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); @@ -267,15 +269,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } get inkDoc() { - return this.props.extensionDoc; + return this.props.Document; } get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. - return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField); + return Cast(this.props.Document.ink, InkField); } set ink(value: InkField | undefined) { - this.props.extensionDoc && (this.props.extensionDoc.ink = value); + this.props.Document.ink = value; } @action @@ -300,7 +302,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[]) => { + getCollection = (selected: Doc[], asTemplate: boolean) => { const bounds = this.Bounds; const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; @@ -322,15 +324,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; // const inkData = this.ink ? this.ink.inkData : undefined; - const newCollection = Docs.Create.FreeformDocument(selected, { + const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; + const newCollection = creator(selected, { x: bounds.left, y: bounds.top, - panX: 0, - panY: 0, + _panX: 0, + _panY: 0, backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - width: bounds.width, - height: bounds.height, + _width: bounds.width, + _height: bounds.height, + _LODdisable: true, title: "a nested collection", }); // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); @@ -353,7 +357,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return d; }); } - const newCollection = this.getCollection(selected); + const newCollection = this.getCollection(selected, e.key === "t"); this.props.addDocument(newCollection); this.props.selectDocuments([newCollection], []); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -361,6 +365,48 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @action + syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const selected = this.marqueeSelect(false); + if (e instanceof KeyboardEvent ? e.key === "i" : true) { + const inks = selected.filter(s => s.proto?.type === "ink"); + const setDocs = selected.filter(s => s.proto?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField)?.Text as string; + }); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + const wordToColor = new Map<string, string>(); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + wordToColor.set(word, colors[i]); + }); + }); + const inkFields = inks.map(i => Cast(i.data, InkField)); + CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => { + const wordResults = results.filter((r: any) => r.category === "inkWord"); + console.log(wordResults); + console.log(results); + for (const word of wordResults) { + const indices: number[] = word.strokeIds; + indices.forEach(i => { + if (wordToColor.has(word.recognizedText.toLowerCase())) { + inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); + } + else { + for (const alt of word.alternates) { + if (wordToColor.has(alt.recognizedString.toLowerCase())) { + inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); + break; + } + } + } + }) + } + }); + } + } + + @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -373,16 +419,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - newCollection.chromeStatus = "disabled"; - const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection._chromeStatus = "disabled"; + const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); newCollection.x = bounds.left + bounds.width; Doc.GetProto(newCollection).summaryDoc = summary; Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - const container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; + const container = Docs.Create.FreeformDocument([summary, newCollection], { + x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true, + _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-" + }); Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" this.props.addLiveTextDocument(container); } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them @@ -405,12 +452,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "s" || e.key === "S") { + if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - if (e.key === "c") { + if (e.key === "c" || e.key === "t") { this.collection(e); } @@ -467,8 +514,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -478,8 +525,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -495,8 +542,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) { selection.push(doc); } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss new file mode 100644 index 000000000..f57ba438a --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -0,0 +1,33 @@ +.collectionMulticolumnView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + + .document-wrapper { + display: flex; + flex-direction: column; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .resizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: column; + + .internal { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx new file mode 100644 index 000000000..041eb69da --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -0,0 +1,258 @@ +import { observer } from 'mobx-react'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; +import * as React from "react"; +import { Doc } from '../../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../../Utils'; +import "./collectionMulticolumnView.scss"; +import { computed, trace, observable, action } from 'mobx'; +import { Transform } from '../../../util/Transform'; +import WidthLabel from './MulticolumnWidthLabel'; +import ResizeBar from './MulticolumnResizer'; +import { undoBatch } from '../../../util/UndoManager'; +import { DragManager } from '../../../util/DragManager'; + +type MulticolumnDocument = makeInterface<[typeof documentSchema]>; +const MulticolumnDocument = makeInterface(documentSchema); + +interface WidthSpecifier { + magnitude: number; + unit: string; +} + +interface LayoutData { + widthSpecifiers: WidthSpecifier[]; + starSum: number; +} + +export const WidthUnit = { + Pixel: "px", + Ratio: "*" +}; + +const resolvedUnits = Object.values(WidthUnit); +const resizerWidth = 4; + +@observer +export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { + + /** + * @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(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio); + } + + /** + * This loops through all childLayoutPairs and extracts the values for widthUnit + * and widthMagnitude, ignoring any that are malformed. Additionally, it then + * normalizes the ratio values so that one * value is always 1, with the remaining + * values proportionate to that easily readable metric. + * @returns the list of the resolved width specifiers (unit and magnitude pairs) + * as well as the sum of the * coefficients, i.e. the ratio magnitudes + */ + @computed + private get resolvedLayoutInformation(): LayoutData { + let starSum = 0; + const widthSpecifiers: WidthSpecifier[] = []; + this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => { + const unit = StrCast(widthUnit); + const magnitude = NumCast(widthMagnitude); + if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { + (unit === WidthUnit.Ratio) && (starSum += magnitude); + widthSpecifiers.push({ magnitude, unit }); + } + /** + * Otherwise, the child document is ignored and the remaining + * space is allocated as if the document were absent from the child list + */ + }); + + /** + * Here, since these values are all relative, adjustments during resizing or + * manual updating can, though their ratios remain the same, cause the values + * themselves to drift toward zero. Thus, whenever we change any of the values, + * we normalize everything (dividing by the smallest magnitude). + */ + setTimeout(() => { + const { ratioDefinedDocs } = this; + if (this.childLayoutPairs.length) { + const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + if (minimum !== 0) { + ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + } + } + }); + + return { widthSpecifiers, starSum }; + } + + /** + * 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 === WidthUnit.Pixel ? magnitude : 0), 0); + } + + /** + * @returns the total quantity, in pixels, that this + * 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. + */ + @computed + private get totalRatioAllocation(): number | undefined { + const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length; + if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); + } + } + + /** + * @returns the total quantity, in pixels, that + * 1* (relative / star unit) is worth. For example, + * if the configuration has three documents, with, respectively, + * widths of 2*, 2* and 1*, and the panel width returns 1000px, + * 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. + */ + @computed + private get columnUnitLength(): number | undefined { + if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { + return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; + } + } + + /** + * This wrapper function exists to prevent mobx from + * needlessly rerendering the internal ContentFittingDocumentViews + */ + private getColumnUnitLength = () => this.columnUnitLength; + + /** + * @param layout the document whose transform we'd like to compute + * Given a layout document, this function + * returns the resolved width it has requested, in pixels. + * @returns the stored column width if already in pixels, + * or the ratio width evaluated to a pixel value + */ + private lookupPixels = (layout: Doc): number => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return 0; // we're still waiting on promises to resolve + } + let width = NumCast(layout.widthMagnitude); + if (StrCast(layout.widthUnit) === WidthUnit.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. + */ + private lookupIndividualTransform = (layout: Doc) => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return Transform.Identity(); // we're still waiting on promises to resolve + } + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.props.ScreenToLocalTransform().translate(-offset, 0); + } + offset += this.lookupPixels(candidate) + resizerWidth; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.widthUnit = "*"; + d.widthMagnitude = 1; + })); + } + return false; + } + + + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + /** + * @returns the resolved list of rendered child documents, displayed + * at their resolved pixel widths, each separated by a resizer. + */ + @computed + private get contents(): JSX.Element[] | null { + const { childLayoutPairs } = this; + const { Document, PanelHeight } = this.props; + const collector: JSX.Element[] = []; + for (let i = 0; i < childLayoutPairs.length; i++) { + const { layout } = childLayoutPairs[i]; + collector.push( + <div + className={"document-wrapper"} + key={Utils.GenerateGuid()} + > + <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + CollectionDoc={this.props.Document} + PanelWidth={() => this.lookupPixels(layout)} + PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)} + getTransform={() => this.lookupIndividualTransform(layout)} + onClick={this.onChildClickHandler} + /> + <WidthLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + width={resizerWidth} + key={Utils.GenerateGuid()} + columnUnitLength={this.getColumnUnitLength} + toLeft={layout} + toRight={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}> + {this.contents} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx new file mode 100644 index 000000000..11e210958 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface ResizerProps { + width: number; + columnUnitLength(): number | undefined; + toLeft?: Doc; + toRight?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const { toLeft, toRight, columnUnitLength } = this.props; + const movingRight = movementX > 0; + const toNarrow = movingRight ? toRight : toLeft; + const toWiden = movingRight ? toLeft : toRight; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const { widthUnit, widthMagnitude } = toNarrow; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + } + if (this.resizeMode === ResizeMode.Pinned && toWiden) { + const { widthUnit, widthMagnitude } = toWiden; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale; + } + } + } + + private get isActivated() { + const { toLeft, toRight } = this.props; + if (toLeft && toRight) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toLeft) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toRight) { + if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"resizer"} + style={{ + width: this.props.width, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx new file mode 100644 index 000000000..b394fed62 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface WidthLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class WidthLabel extends React.Component<WidthLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.widthUnit); + const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3)); + return ( + <div className={"label-wrapper"}> + <EditableView + GetValue={getMagnitude} + SetValue={value => { + const converted = Number(value); + if (!isNaN(converted) && converted > 0) { + layout.widthMagnitude = converted; + return true; + } + return false; + }} + contents={getMagnitude()} + /> + <EditableView + GetValue={getUnit} + SetValue={value => { + if (Object.values(WidthUnit).includes(value)) { + layout.widthUnit = value; + return true; + } + return false; + }} + contents={getUnit()} + /> + </div> + ); + } + + render() { + return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null); + } + +}
\ No newline at end of file |
