diff options
Diffstat (limited to 'src/client/views/collections')
32 files changed, 1392 insertions, 922 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index fd1296286..a9a1898f5 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -13,6 +13,7 @@ height: calc(100% - 50px); display: inline-block; width: 100%; + user-select: none; } } .carouselView-back, .carouselView-fwd { diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index a0cb1fe19..9e7248db2 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -80,10 +80,33 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) }); } } + _downX = 0; + _downY = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downX = e.clientX; + this._downY = e.clientY; + console.log("CAROUSEL down"); + document.addEventListener("pointerup", this.onpointerup); + } + private _lastTap: number = 0; + private _doubleTap = false; + onpointerup = (e: PointerEvent) => { + console.log("CAROUSEL up"); + this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); + this._lastTap = Date.now(); + } + + onClick = (e: React.MouseEvent) => { + if (this._doubleTap) { + e.stopPropagation(); + this.props.Document.isLightboxOpen = true; + } + } + render() { - return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> + return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> {this.content} - {this.buttons} + {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1e38a8927..28aaf0c57 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faFile } from '@fortawesome/free-solid-svg-icons'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; @@ -15,12 +15,12 @@ import { FieldId } from "../../../new_fields/RefField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { emptyFunction, returnOne, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager } from "../../util/DragManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; @@ -127,36 +127,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action public static CloseRightSplit(document: Opt<Doc>): boolean { - if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; - 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 && 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(); + const tryClose = (childItem: any) => { + if (childItem.config?.component === "DocumentFrameRenderer") { + const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId); + if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) { + childItem.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) && - ((!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; - } - return false; - }); } - return false; - }); - } - if (retVal) { - instance.stateChanged(); - } + } + return false; + }; + const retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false : + Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose)); + + retVal && instance.stateChanged(); return retVal; } @@ -238,6 +224,75 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return true; } + + // + // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side + // + @undoBatch + @action + public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) { + if (!CollectionDockingView.Instance) return false; + const instance = CollectionDockingView.Instance; + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] + }; + + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row + if (pullSide === "left") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); + } else if (pullSide === "right") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem); + } else if (pullSide === "top" || pullSide === "bottom") { + // if not going in a row layout, must add already existing content into column + const rowlayout = instance._goldenLayout.root.contentItems[0]; + const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + rowlayout.parent.replaceChild(rowlayout, newColumn); + if (pullSide === "top") { + newColumn.addChild(rowlayout, undefined, true); + newColumn.addChild(newContentItem, 0, true); + } else if (pullSide === "bottom") { + newColumn.addChild(newContentItem, undefined, true); + newColumn.addChild(rowlayout, 0, true); + } + + rowlayout.config.height = 50; + newContentItem.config.height = 50; + } + } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column + if (pullSide === "top") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); + } else if (pullSide === "bottom") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem); + } else if (pullSide === "left" || pullSide === "right") { + // if not going in a row layout, must add already existing content into column + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + if (pullSide === "left") { + newRow.addChild(collayout, undefined, true); + newRow.addChild(newContentItem, 0, true); + } else if (pullSide === "right") { + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + } + + collayout.config.width = 50; + newContentItem.config.width = 50; + } + } + + 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 // @@ -434,6 +489,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp }; tab.titleElement[0].size = StrCast(doc.title).length + 1; tab.titleElement[0].value = doc.title; + tab.titleElement[0].style["max-width"] = "100px"; const gearSpan = document.createElement("span"); gearSpan.className = "collectionDockingView-gear"; gearSpan.style.position = "relative"; @@ -449,15 +505,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } tab.setActive(true); }; - ReactDOM.render(<span title="Drag as document" - className="collectionDockingView-dragAsDocument" - onPointerDown={e => { + const onDown = (e: React.PointerEvent) => { + if (!(e.nativeEvent as any).defaultPrevented) { e.preventDefault(); e.stopPropagation(); const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction === "alias" ? "alias" : doc.dropAction === "copy" ? "copy" : undefined; + dragData.dropAction = doc.dropAction as dropActionType; DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - }}><DockingViewButtonSelector Document={doc} Stack={stack} /></span>, gearSpan); + } + }; + let rendered = false; + tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), + (views) => { + !rendered && ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > + <DockingViewButtonSelector views={views} Stack={stack} /> + </span>, + gearSpan); + rendered = true; + }); + tab.reactComponents = [gearSpan]; tab.element.append(gearSpan); tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { @@ -471,7 +537,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } tab.closeElement.off('click') //unbind the current click handler .click(async function () { - tab.reactionDisposer && tab.reactionDisposer(); + tab.reactionDisposer?.(); + tab.buttonDisposer?.(); const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); if (doc instanceof Doc) { const theDoc = doc; @@ -721,11 +788,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { Document={document} DataDoc={resolvedDataDoc} bringToFront={emptyFunction} + rootSelected={returnTrue} addDocument={undefined} removeDocument={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} ScreenToLocalTransform={this.ScreenToLocalTransform} renderDepth={0} parentActive={returnTrue} @@ -735,9 +805,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { addDocTab={this.addDocTab} pinToPres={DockedFrameRenderer.PinDoc} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne} />; + ContainingCollectionDoc={undefined} />; } render() { diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index eae9e0220..123a27deb 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -8,6 +8,8 @@ display:flex; height: 100%; >label { + margin-top: "auto"; + margin-bottom: "auto"; background: $dark-color; color: $light-color; display: inline-block; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 4a14a30cd..cb0206260 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -3,8 +3,8 @@ 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 { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse, returnZero } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { Transform } from '../../util/Transform'; import "./CollectionLinearView.scss"; @@ -13,7 +13,6 @@ 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,]>; @@ -28,12 +27,10 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { 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); - }); + this._dropDisposer?.(); + this._widthDisposer?.(); + this._selectedDisposer?.(); + this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); } componentDidMount() { @@ -48,17 +45,17 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { (i) => runInAction(() => { this._selectedIndex = i; let selected: any = undefined; - this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => { + this.childLayoutPairs.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); + ScriptCast(pair.layout.proto?.onPointerUp)?.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); + ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); } }), { fireImmediately: true } @@ -71,32 +68,37 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } - public isCurrent(doc: Doc) { return (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); + return new Transform(-translateX, -translateY, 1); } render() { const guid = Utils.GenerateGuid(); + const flexDir: any = StrCast(this.Document.flexDirection); + const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); + const color = StrCast(this.props.Document.color, "white"); return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > + <label htmlFor={`${guid}`} title="Close Menu" style={{ background: backgroundColor === color ? "black" : backgroundColor }} + onPointerDown={e => e.stopPropagation()} > + <p>+</p> + </label> <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} onChange={action((e: any) => this.props.Document.linearViewIsExpanded = 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) }}> - {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> + {this.childLayoutPairs.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} + const scalable = pair.layout.onClick || pair.layout.onDragStart; + return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ - width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, + width: scalable ? (nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize) : undefined, height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, }} > <DocumentView @@ -107,10 +109,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} pinToPres={emptyFunction} + rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} 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} @@ -120,10 +125,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView> + ContainingCollectionDoc={undefined} /> </div>; })} </div> diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c9b7ca221..b272151c1 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -8,7 +8,7 @@ import Measure from "react-measure"; import { Doc } from "../../../new_fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { StrCast } from "../../../new_fields/Types"; +import { StrCast, NumCast } from "../../../new_fields/Types"; import { numberRange } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; @@ -36,6 +36,9 @@ interface CMVFieldRowProps { createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; + showHandle: boolean; } @observer @@ -53,14 +56,19 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr private _contRef: React.RefObject<HTMLDivElement> = React.createRef(); private _sensitivity: number = 16; private _counter: number = 0; - + private _ele: any; createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { + this._ele = ele; + this.props.observeHeight(ele); this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } getTrueHeight = () => { if (this._collapsed) { @@ -75,11 +83,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { + console.log("masronry row drop"); this._createAliasSelected = false; if (de.complete.docDragData) { (this.props.parent.Document.dropConverter instanceof ScriptField) && this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(this._heading); de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); this.props.parent.onInternalDrop(e, de); @@ -98,7 +107,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headingChanged = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(value); if (castedValue) { if (this.props.parent.sectionHeaders) { @@ -137,15 +146,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action 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 key = StrCast(this.props.parent.props.Document._pivotField); + const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value }); newDoc[key] = this.getValue(this.props.heading); - return this.props.parent.props.addDocument(newDoc); + const docs = this.props.parent.childDocList; + return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc); } deleteRow = undoBatch(action(() => { this._createAliasSelected = false; - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); this.props.docList.forEach(d => d[key] = undefined); if (this.props.parent.sectionHeaders && this.props.headingObject) { const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); @@ -167,7 +177,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { const alias = Doc.MakeAlias(this.props.parent.props.Document); - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); let value = this.getValue(this._heading); value = typeof value === "string" ? `"${value}"` : value; const script = `return doc.${key} === ${value}`; @@ -273,6 +283,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr }; return collapsed ? (null) : <div style={{ position: "relative" }}> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div className="collectionStackingView-addDocumentButton" + style={{ + width: style.columnWidth / style.numGroupColumns, + padding: NumCast(this.props.parent.layoutDoc._yPadding) + }}> + <EditableView {...newEditableViewProps} /> + </div> : null + } <div className={`collectionStackingView-masonryGrid`} ref={this._contRef} style={{ @@ -282,20 +301,14 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), }}> {this.props.parent.children(this.props.docList)} - {this.props.parent.columnDragger} + {this.props.showHandle && this.props.parent.props.active() ? this.props.parent.columnDragger : (null)} </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? - <div className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> - </div> : null - } </div>; } @computed get headingView() { const heading = this._heading; - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { GetValue: () => evContents, @@ -316,7 +329,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} - style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", }}> + style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey" }}> <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index facde3648..82204ca7b 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -4,8 +4,9 @@ import { observer } from "mobx-react"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; +import { KeyCodes } from "../../util/KeyCodes"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; @@ -21,7 +22,6 @@ import { SelectionManager } from "../../util/SelectionManager"; import { library } from '@fortawesome/fontawesome-svg-core'; 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"; library.add(faExpand); @@ -76,17 +76,28 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @action isEditingCallback = (isEditing: boolean): void => { - document.addEventListener("keydown", this.onKeyDown); + document.removeEventListener("keydown", this.onKeyDown); + isEditing && document.addEventListener("keydown", this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); } @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,6 +156,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { Document: this.props.rowProps.original, DataDoc: this.props.rowProps.original, LibraryPath: [], + dropAction: "alias", + bringToFront: emptyFunction, + rootSelected: returnFalse, fieldKey: this.props.rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, @@ -157,6 +171,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { whenActiveChanged: emptyFunction, PanelHeight: returnZero, PanelWidth: returnZero, + NativeHeight: returnZero, + NativeWidth: returnZero, addDocTab: this.props.addDocTab, pinToPres: this.props.pinToPres, ContentScaling: returnOne diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 6eeceb552..57be77fdd 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -12,9 +12,8 @@ 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, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { Gateway } from "../../northstar/manager/Gateway"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -28,7 +27,8 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; +import { setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils"; +import { DocumentView } from "../nodes/DocumentView"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -51,8 +51,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { - private _mainCont?: HTMLDivElement; - private _startPreviewWidth = 0; + private _previewCont?: HTMLDivElement; private DIVIDER_WIDTH = 4; @observable previewDoc: Doc | undefined = undefined; @@ -64,7 +63,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } private createTarget = (ele: HTMLDivElement) => { - this._mainCont = ele; + this._previewCont = ele; super.CreateDropTarget(ele); } @@ -81,12 +80,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } onDividerDown = (e: React.PointerEvent) => { - this._startPreviewWidth = this.previewWidth(); setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); } @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - const nativeWidth = this._mainCont!.getBoundingClientRect(); + const nativeWidth = this._previewCont!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; @@ -119,27 +117,32 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewPanel() { - const layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; - return <div ref={this.createTarget}> - <ContentFittingDocumentView - Document={layoutDoc} - DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} - LibraryPath={this.props.LibraryPath} - childDocs={this.childDocs} - renderDepth={this.props.renderDepth} - PanelWidth={this.previewWidth} - PanelHeight={this.previewHeight} - getTransform={this.getPreviewTransform} - CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} - CollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - active={this.props.active} - whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - /> + return <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}> + {!this.previewDocument ? (null) : + <ContentFittingDocumentView + Document={this.previewDocument} + DataDocument={undefined} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={true} + FreezeDimensions={true} + focus={emptyFunction} + LibraryPath={this.props.LibraryPath} + renderDepth={this.props.renderDepth} + rootSelected={this.rootSelected} + PanelWidth={this.previewWidth} + PanelHeight={this.previewHeight} + getTransform={this.getPreviewTransform} + CollectionDoc={this.props.CollectionView?.props.Document} + CollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + />} </div>; } @@ -182,7 +185,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { render() { return <div className="collectionSchemaView-container"> - <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable} </div> {this.dividerDragger} @@ -669,27 +672,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } } - @action - makeDB = async () => { - let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); - csv = csv.substr(0, csv.length - 1) + "\n"; - const self = this; - this.childDocs.map(doc => { - csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", ""); - csv = csv.substr(0, csv.length - 1) + "\n"; - }); - csv.substring(0, csv.length - 1); - const dbName = StrCast(this.props.Document.title); - const res = await Gateway.Instance.PostSchema(csv, dbName); - if (self.props.CollectionView && self.props.CollectionView.props.addDocument) { - const schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); - if (schemaDoc) { - //self.props.CollectionView.props.addDocument(schemaDoc, false); - self.props.Document.schemaDoc = schemaDoc; - } - } - } - getField = (row: number, col?: number) => { const docs = this.childDocs; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index bfa5ea278..47faa9239 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -26,6 +26,9 @@ position: relative; display: block; } + .collectionStackingViewFieldColumn { + height:max-content; + } .collectionSchemaView-previewDoc { height: 100%; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index d1f45af90..da53888fc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -11,8 +11,8 @@ import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from "../../../new_fields/util"; -import { Utils, setupMoveUpEvents, emptyFunction } from "../../../Utils"; -import { DragManager } from "../../util/DragManager"; +import { Utils, setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; @@ -24,35 +24,37 @@ import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; -import { Docs } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); - _heightDisposer?: IReactionDisposer; - _sectionFilterDisposer?: IReactionDisposer; + _pivotFieldDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @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 pivotField() { return StrCast(this.props.Document._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } @computed get yMargin() { return Math.max(this.props.Document._showTitle && !this.props.Document._showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 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.pivotField && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); } @computed get columnWidth() { + TraceMobx(); 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; } children(docs: Doc[], columns?: number) { + TraceMobx(); this._docXfs.length = 0; return docs.map((d, i) => { const height = () => this.getDocHeight(d); @@ -63,7 +65,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(d, this.props.DataDoc, dxf, width)} + {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)} </div>; }); } @@ -73,7 +75,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } get Sections() { - if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); + if (!this.pivotField || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); if (this.sectionHeaders === undefined) { setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); @@ -83,66 +85,60 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { - const sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object; + const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object; // the next five lines ensures that floating point rounding errors don't create more than one section -syip const parsed = parseInt(sectionValue.toString()); const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; // look for if header exists already - const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`)); + const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); if (existingHeader) { fields.get(existingHeader)!.push(d); } else { - const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`); + const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); sectionHeaders.push(newSchemaHeader); changed = true; } }); + // remove all empty columns if hideHeadings is set + if (this.props.Document.hideHeadings) { + Array.from(fields.keys()).filter(key => !fields.get(key)!.length).map(header => { + fields.delete(header); + sectionHeaders.splice(sectionHeaders.indexOf(header), 1); + changed = true; + }); + } changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0); return fields; } + getSimpleDocHeight(d?: Doc) { + if (!d) return 0; + const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); + if (!layoutDoc._fitWidth && nw && nh) { + const aspect = nw && nh ? nh / nw : 1; + if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + return wid * aspect; + } + return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym](); + } componentDidMount() { super.componentDidMount(); - this._heightDisposer = reaction(() => { - 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.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); - return val; - }, this.yMargin)); - return r1; - }, 0); - return res; - } else { - const sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); - return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15)); - } - } - return -1; - }, - (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); - }, - { fireImmediately: true } - ); // reset section headers when a new filter is inputted - this._sectionFilterDisposer = reaction( - () => this.sectionFilter, + this._pivotFieldDisposer = reaction( + () => this.pivotField, () => this.props.Document.sectionHeaders = new List() ); } componentWillUnmount() { super.componentWillUnmount(); - this._heightDisposer && this._heightDisposer(); - this._sectionFilterDisposer && this._sectionFilterDisposer(); + this._pivotFieldDisposer?.(); } @action @@ -162,18 +158,23 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const height = () => this.getDocHeight(doc); return <ContentFittingDocumentView Document={doc} - DataDocument={doc[DataSym] !== doc && doc[DataSym]} + DataDocument={dataDoc || (doc[DataSym] !== doc && doc[DataSym])} backgroundColor={this.props.backgroundColor} LayoutDoc={this.props.childLayoutTemplate} LibraryPath={this.props.LibraryPath} + FreezeDimensions={this.props.freezeChildDimensions} renderDepth={this.props.renderDepth + 1} - fitToBox={this.props.fitToBox} - onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} PanelHeight={height} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + rootSelected={this.rootSelected} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} getTransform={dxf} focus={this.props.focus} - CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} + CollectionDoc={this.props.CollectionView?.props.Document} CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} @@ -276,9 +277,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } }); } - headings = () => Array.from(this.Sections.keys()); + headings = () => Array.from(this.Sections); + refList: any[] = []; sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { - const key = this.sectionFilter; + const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; 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) { @@ -287,6 +289,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionStackingViewFieldColumn + unobserveHeight={(ref) => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={(ref) => { + if (ref) { + this.refList.push(ref); + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + this.observer = new _global.ResizeObserver(action((entries: any) => { + if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) { + Doc.Layout(doc)._height = Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))); + } + })); + this.observer.observe(ref); + } + }} key={heading ? heading.heading : ""} cols={cols} headings={this.headings} @@ -305,15 +320,13 @@ 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); - 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); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + offsety); } - sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { - const key = this.sectionFilter; + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { + const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; 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) { @@ -322,6 +335,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionMasonryViewFieldRow + showHandle={first} + unobserveHeight={(ref) => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={(ref) => { + if (ref) { + this.refList.push(ref); + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + this.observer = new _global.ResizeObserver(action((entries: any) => { + if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) { + Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + } + })); + this.observer.observe(ref); + } + }} key={heading ? heading.heading : ""} rows={rows} headings={this.headings} @@ -341,7 +368,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (value && this.sectionHeaders) { const schemaHdrField = new SchemaHeaderField(value); this.sectionHeaders.push(schemaHdrField); - Doc.addEnumerationToTextField(undefined, this.sectionFilter, [Docs.Create.TextDocument(value, { title: value, _backgroundColor: schemaHdrField.color })]); + Doc.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]); return true; } return false; @@ -370,15 +397,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get renderedSections() { TraceMobx(); let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; - if (this.sectionFilter) { + if (this.pivotField) { const entries = Array.from(this.Sections.entries()); sections = entries.sort(this.sortFunc); } - return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1])); + return sections.map((section, i) => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)); } - @computed get scaling() { return !this.props.Document._nativeWidth ? 1 : this.props.PanelHeight() / NumCast(this.props.Document._nativeHeight); } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth) || this.props.NativeWidth() || 0; } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight) || this.props.NativeHeight() || 0; } + + @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } + + observer: any; render() { TraceMobx(); const editableViewProps = { @@ -407,7 +439,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { 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' && this.props.isSelected() ? <Switch onChange={this.onToggle} onClick={this.onToggle} defaultChecked={this.props.Document._chromeStatus !== 'view-mode'} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 516e583d4..5d926b7c7 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -23,7 +23,6 @@ import { CollectionStackingView } from "./CollectionStackingView"; import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; import { listSpec } from "../../../new_fields/Schema"; -import { Schema } from "prosemirror-model"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -40,6 +39,8 @@ interface CSVFieldColumnProps { type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; + observeHeight: (myref: any) => void; + unobserveHeight: (myref: any) => void; } @observer @@ -51,18 +52,24 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + _ele: HTMLElement | null = null; createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { + this._ele = ele; + this.props.observeHeight(ele); this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } } + componentWillUnmount() { + this.props.unobserveHeight(this._ele); + } @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(this._heading); de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, false)); this.props.parent.onInternalDrop(e, de); @@ -85,7 +92,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action headingChanged = (value: string, shiftDown?: boolean) => { - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(value); if (castedValue) { if (this.props.parent.sectionHeaders) { @@ -126,18 +133,19 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action addDocument = (value: string, shiftDown?: boolean) => { if (!value) return false; - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); 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; newDoc.heading = heading; - return this.props.parent.props.addDocument(newDoc); + this.props.parent.props.addDocument(newDoc); + return false; } @action deleteColumn = () => { - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); this.props.docList.forEach(d => d[key] = undefined); if (this.props.parent.sectionHeaders && this.props.headingObject) { const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); @@ -161,8 +169,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC startDrag = (e: PointerEvent, down: number[], delta: number[]) => { const alias = Doc.MakeAlias(this.props.parent.props.Document); alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.props.Document.sectionHeaders, listSpec(SchemaHeaderField))?.length || 1); - alias.sectionFilter = undefined; - const key = StrCast(this.props.parent.props.Document.sectionFilter); + alias._pivotField = undefined; + const key = StrCast(this.props.parent.props.Document._pivotField); let value = this.getValue(this._heading); value = typeof value === "string" ? `"${value}"` : value; alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); @@ -277,7 +285,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC render() { TraceMobx(); const cols = this.props.cols(); - const key = StrCast(this.props.parent.props.Document.sectionFilter); + const key = StrCast(this.props.parent.props.Document._pivotField); let templatecols = ""; const headings = this.props.headings(); const heading = this._heading; @@ -353,7 +361,12 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; 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 }} + <div className="collectionStackingViewFieldColumn" key={heading} + style={{ + width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, + height: SelectionManager.GetIsDragging() ? "100%" : undefined, + background: this._background + }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {this.props.parent.Document.hideHeadings ? (null) : headingView} { diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 8c7e113b2..5b9a69bf7 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -1,22 +1,20 @@ import { CollectionSubView } from "./CollectionSubView"; -import { Transform } from "../../util/Transform"; import React = require("react"); import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx"; -import { Doc } from "../../../new_fields/Doc"; import { NumCast } from "../../../new_fields/Types"; import "./CollectionStaffView.scss"; import { observer } from "mobx-react"; @observer export class CollectionStaffView extends CollectionSubView(doc => doc) { - private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(0, -this._mainCont.current!.scrollTop); - private _mainCont = React.createRef<HTMLDivElement>(); private _reactionDisposer: IReactionDisposer | undefined; @observable private _staves = NumCast(this.props.Document.staves); + componentWillUnmount() { + this._reactionDisposer?.(); + } componentDidMount = () => { - this._reactionDisposer = reaction( - () => NumCast(this.props.Document.staves), + this._reactionDisposer = reaction(() => NumCast(this.props.Document.staves), (staves) => runInAction(() => this._staves = staves) ); @@ -47,7 +45,7 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) { } render() { - return <div className="collectionStaffView" ref={this._mainCont}> + return <div className="collectionStaffView"> {this.staves} {this.addStaffButton} </div>; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6ad187e5c..41f4fb3f0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,12 +1,11 @@ import { action, computed, IReactionDisposer, reaction } from "mobx"; -import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; @@ -35,30 +34,36 @@ export interface CollectionViewProps extends FieldViewProps { PanelHeight: () => number; VisibleHeight?: () => number; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + rootSelected: (outsideReaction?: boolean) => boolean; fieldKey: string; + NativeWidth: () => number; + NativeHeight: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; children?: never | (() => JSX.Element[]) | React.ReactNode; - overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explict list (see LinkBox) + freezeChildDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height + overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) ignoreFields?: string[]; // used in TreeView to ignore specified fields (see LinkBox) isAnnotationOverlay?: boolean; annotationsKey: string; layoutEngine?: () => string; } -export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { - class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { +export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) { + class CollectionSubView extends DocComponent<X & SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; + protected _mainCont?: HTMLDivElement; protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer?.(); this.gestureDisposer?.(); this.multiTouchDisposer?.(); if (ele) { + this._mainCont = ele; this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); @@ -90,10 +95,14 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } @computed get dataDoc() { - return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : + return (this.props.DataDoc instanceof Doc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template } + rootSelected = (outsideReaction?: boolean) => { + return this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument || this.props.Document.forceActive ? this.props.rootSelected(outsideReaction) : false); + } + // 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. @@ -105,15 +114,62 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, 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 + return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { return Cast(this.dataField, listSpec(Doc)); } - get childDocs() { - const docs = DocListCast(this.dataField); + @computed get childDocs() { + const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []); + const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []); + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + for (let i = 0; i < docFilters.length; i += 3) { + const [key, value, modifiers] = docFilters.slice(i, i + 3); + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + } + + let rawdocs: (Doc | Promise<Doc>)[] = []; + if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; + rawdocs = [this.dataField]; + } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it. + rawdocs = Cast(this.dataField, listSpec(Doc), null); + } else { // Finally, if it's not a doc or a list and the document is a template, we try to render the root doc. + // For example, if an image doc is rendered with a slide template, the template will try to render the data field as a collection. + // Since the data field is actually an image, we set the list of documents to the singleton of root document's proto which will be an image. + const rootDoc = Cast(this.props.Document.rootDocument, Doc, null); + rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; + } + const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + + const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => { + for (const facetKey of Object.keys(filterFacets)) { + const facet = filterFacets[facetKey]; + const satisfiesFacet = Object.keys(facet).some(value => + (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value)); + if (!satisfiesFacet) { + return false; + } + } + return true; + }) : childDocs; + const rangeFilteredDocs = filteredDocs.filter(d => { + for (let i = 0; i < docRangeFilters.length; i += 3) { + const key = docRangeFilters[i]; + const min = Number(docRangeFilters[i + 1]); + const max = Number(docRangeFilters[i + 2]); + const val = Cast(d[key], "number", null); + if (val !== undefined && (val < min || val > max)) { + return false; + } + } + return true; + }); + return rangeFilteredDocs; } @action @@ -150,7 +206,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { - } @undoBatch @@ -161,9 +216,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this if (docDragData) { let added = false; - if (this.props.Document._freezeOnDrop) { - de.complete.docDragData?.droppedDocuments.forEach(drop => Doc.freezeNativeDimensions(drop, drop[WidthSym](), drop[HeightSym]())); - } if (docDragData.dropAction || docDragData.userDropAction) { added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (docDragData.moveDocument) { @@ -231,7 +283,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (!html.startsWith("<a")) { const tags = html.split("<"); if (tags[0] === "") tags.splice(0, 1); - const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; + let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; + const cors = img.includes("corsProxy") ? img.match(/http.*corsProxy\//)![0] : ""; + img = cors ? img.replace(cors, "") : img; if (img) { const split = img.split("src=\"")[1].split("\"")[0]; let source = split; @@ -239,9 +293,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [split] }); source = Utils.prepend(accessPaths.agnostic.client); } - const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 }); - ImageUtils.ExtractExif(doc); - addDocument(doc); + if (source.startsWith("http")) { + const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 }); + ImageUtils.ExtractExif(doc); + addDocument(doc); + } return; } else { const path = window.location.origin + "/doc/"; @@ -309,7 +365,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.includes("uri")) { const stringContents = await new Promise<string>(resolve => item.getAsString(resolve)); - const type = (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; + const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; if (type) { const doc = await Docs.Get.DocumentFromType(type, stringContents, options); doc && generatedDocuments.push(doc); diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss index 865fc3cd2..fa7c87f4e 100644 --- a/src/client/views/collections/CollectionTimeView.scss +++ b/src/client/views/collections/CollectionTimeView.scss @@ -47,7 +47,8 @@ .collectionTimeView-flyout { width: 400px; - display: inline-block; + display: block; + text-align: left; .collectionTimeView-flyout-item { background-color: lightgray; @@ -72,62 +73,11 @@ } } - .collectionTimeView-treeView { - display: flex; - flex-direction: column; - width: 200px; - height: 100%; - - .collectionTimeView-addfacet { - display: inline-block; - width: 200px; - height: 30px; - background: darkGray; - text-align: center; - - .collectionTimeView-button { - align-items: center; - display: flex; - width: 100%; - height: 100%; - - .collectionTimeView-span { - margin: auto; - } - } - - >div, - >div>div { - width: 100%; - height: 100%; - text-align: center; - } - } - - .collectionTimeView-tree { - display: inline-block; - width: 100%; - height: calc(100% - 30px); - } - } - .collectionTimeView-innards { display: inline-block; width: calc(100% - 200px); height: 100%; } - - .collectionTimeView-dragger { - background-color: lightgray; - height: 40px; - width: 20px; - position: absolute; - border-radius: 10px; - top: 55%; - border: 1px black solid; - z-index: 2; - left: -10px; - } } .collectionTimeView-pivot { diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 50e297f0b..53de2fbbe 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,16 +1,12 @@ -import { faEdit } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction, trace } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Field, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { ObjectField } from "../../../new_fields/ObjectField"; import { RichTextField } from "../../../new_fields/RichTextField"; -import { listSpec } from "../../../new_fields/Schema"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Docs } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; +import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; @@ -19,7 +15,6 @@ import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngi import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTimeView.scss"; -import { CollectionTreeView } from "./CollectionTreeView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -29,250 +24,55 @@ import React = require("react"); export class CollectionTimeView extends CollectionSubView(doc => doc) { _changing = false; @observable _layoutEngine = "pivot"; - + @observable _collapsed: boolean = false; + componentWillUnmount() { + this.props.Document.onChildClick = undefined; + } componentDidMount() { - this.props.Document._freezeOnDrop = true; 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, treeViewHideHeaderFields: true }); - facetCollection.target = this.props.Document; - this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilters"]); - - const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)"; - const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; - 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, shiftKey: "boolean" }); - this.props.Document._facetCollection = facetCollection; - this.props.Document._fitToBox = true; - } + const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(thisContainer.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", thisContainer: Doc.name, shiftKey: "boolean" }); + this.props.Document._fitToBox = true; if (!this.props.Document.onViewDefClick) { this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); } } - bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth; - getTransform = () => this.props.ScreenToLocalTransform().translate(-this._facetWidth, 0); - - @computed get _allFacets() { - const facets = new Set<string>(); - this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets); - } - - /** - * 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._docFilters, listSpec("string")); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); - } - } - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string")); - if (docRangeFilters) { - let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); - } - } - } else { - const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => - set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); - - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - if (nonNumbers / allCollectionDocs.length < .1) { - const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); - const newFacet = Docs.Create.SliderDocument({ title: facetHeader }); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet.treeViewExpandedView = "layout"; - newFacet.treeViewOpen = true; - newFacet._sliderMin = ranged === undefined ? minVal : ranged[0]; - newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1]; - newFacet._sliderMinThumb = minVal; - newFacet._sliderMaxThumb = maxVal; - newFacet.target = this.props.Document; - const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); - - Doc.AddDocToList(facetCollection, "data", newFacet); - } 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 = 0; - 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); - } - - menuCallback = (x: number, y: number) => { - ContextMenu.Instance.clearItems(); - const docItems: ContextMenuProps[] = []; - const keySet: Set<string> = new Set(); - - this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey => - pair.layout[fieldKey] instanceof RichTextField || - typeof (pair.layout[fieldKey]) === "number" || - typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey))); - Array.from(keySet).map(fieldKey => - docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" })); - docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" }); - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(x, y, ":"); - } - - @observable private collapsed: boolean = false; - private toggleVisibility = action(() => this.collapsed = !this.collapsed); + layoutEngine = () => this._layoutEngine; + toggleVisibility = action(() => this._collapsed = !this._collapsed); - _downX = 0; onMinDown = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onMinMove); - document.removeEventListener("pointerup", this.onMinUp); - document.addEventListener("pointermove", this.onMinMove); - document.addEventListener("pointerup", this.onMinUp); - this._downX = e.clientX; - e.stopPropagation(); - e.preventDefault(); - } - @action - onMinMove = (e: PointerEvent) => { - const delta = e.clientX - this._downX; - this._downX = e.clientX; - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined; - } - onMinUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onMinMove); - document.removeEventListener("pointermove", this.onMinUp); + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); + this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined; + return false; + }), returnFalse, emptyFunction); } onMaxDown = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onMaxMove); - document.removeEventListener("pointermove", this.onMaxUp); - document.addEventListener("pointermove", this.onMaxMove); - document.addEventListener("pointerup", this.onMaxUp); - this._downX = e.clientX; - e.stopPropagation(); - e.preventDefault(); - } - @action - onMaxMove = (e: PointerEvent) => { - const delta = e.clientX - this._downX; - this._downX = e.clientX; - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined; - } - onMaxUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onMaxMove); - document.removeEventListener("pointermove", this.onMaxUp); + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); + this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); + return false; + }), returnFalse, emptyFunction); } onMidDown = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onMidMove); - document.removeEventListener("pointermove", this.onMidUp); - document.addEventListener("pointermove", this.onMidMove); - document.addEventListener("pointerup", this.onMidUp); - this._downX = e.clientX; - e.stopPropagation(); - e.preventDefault(); - } - @action - onMidMove = (e: PointerEvent) => { - const delta = e.clientX - this._downX; - this._downX = e.clientX; - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta / this.props.PanelWidth(); - } - onMidUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onMidMove); - document.removeEventListener("pointermove", this.onMidUp); + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); + this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); + return false; + }), returnFalse, emptyFunction); } - layoutEngine = () => this._layoutEngine; @computed get contents() { - return <div className="collectionTimeView-innards" key="timeline" style={{ width: this.bodyPanelWidth() }}> - <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} /> - </div>; - } - @computed get filterView() { - trace(); - const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null); - const flyout = ( - <div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}`, height: this.props.PanelHeight() - 30, display: "block" }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="collectionTimeView-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 <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}> - <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="collectionTimeView-button"> - <span className="collectionTimeView-span">Facet Filters</span> - <FontAwesomeIcon icon={faEdit} size={"lg"} /> - </div> - </Flyout> - </div> - <div className="collectionTimeView-tree" key="tree"> - <CollectionTreeView {...this.props} PanelWidth={() => this._facetWidth} Document={facetCollection} /> - </div> + return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }}> + <CollectionFreeFormView {...this.props} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} /> </div>; } @@ -291,8 +91,30 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" }); } + @computed get _allFacets() { + const facets = new Set<string>(); + this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); + Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key))); + return Array.from(facets); + } + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const docItems: ContextMenuProps[] = []; + const keySet: Set<string> = new Set(); + + this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey => + pair.layout[fieldKey] instanceof RichTextField || + typeof (pair.layout[fieldKey]) === "number" || + typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey))); + Array.from(keySet).map(fieldKey => + docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" })); + docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" }); + ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" }); + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + ContextMenu.Instance.displayMenu(x, y, ":"); + } - render() { + @computed get pivotKeyUI() { const newEditableViewProps = { GetValue: () => "", SetValue: (value: any) => { @@ -307,7 +129,26 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { toggle: this.toggleVisibility, color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; }; + return <div className={"pivotKeyEntry"}> + <button className="collectionTimeView-backBtn" + onClick={action(() => { + let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex); + if (prevFilterIndex > 0) { + prevFilterIndex--; + this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField); + this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); + this.props.Document._prevFilterIndex = prevFilterIndex; + } else { + this.props.Document._docFilters = new List([]); + } + })}> + back + </button> + <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} /> + </div>; + } + render() { let nonNumbers = 0; this.childDocs.map(doc => { const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)]))); @@ -327,41 +168,16 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { } } - - const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null); - return !facetCollection ? (null) : - <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu} - style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> - <div className={"pivotKeyEntry"}> - <button className="collectionTimeView-backBtn" - onClick={action(() => { - let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex); - if (prevFilterIndex > 0) { - prevFilterIndex--; - this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField); - this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); - this.props.Document._prevFilterIndex = prevFilterIndex; - } else { - this.props.Document._docFilters = new List([]); - } - })}> - back - </button> - <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} /> - </div> - {!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) : - <div className="collectionTimeView-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> - } - {this.filterView} - {this.contents} - {!this.props.isSelected() || !doTimeline ? (null) : <> - <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} /> - <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} /> - <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} /> - </>} - </div>; + return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu} + style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> + {this.pivotKeyUI} + {this.contents} + {!this.props.isSelected() || !doTimeline ? (null) : <> + <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} /> + <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} /> + <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} /> + </>} + </div>; } } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 6ebe81545..8e95f7fbe 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -22,6 +22,7 @@ ul { list-style: none; padding-left: 20px; + margin-bottom: 1px;// otherwise vertical scrollbars may pop up for no apparent reason.... } @@ -34,7 +35,9 @@ width: 15px; color: $intermediate-color; margin-top: 3px; - transform: scale(1.3, 1.3); + transform: scale(1.3, 1.3); + border: #80808030 1px solid; + border-radius: 4px; } .editableView-container { @@ -123,6 +126,9 @@ .editableView-container-editing-oneLine { min-width: 15px; } + .documentView-node-topmost { + width: unset; + } } .treeViewItem-header-above { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 7eeeb6ff1..7edda5a4f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -6,11 +6,11 @@ import { observer } from "mobx-react"; import { Doc, DocListCast, Field, HeightSym, WidthSym, DataSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { Document, listSpec } from '../../../new_fields/Schema'; +import { Document, listSpec, createSchema, makeInterface } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; 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 { emptyFunction, emptyPath, returnFalse, Utils, returnOne, returnZero, returnTransparent, returnTrue } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; @@ -29,11 +29,12 @@ import { ImageBox } from '../nodes/ImageBox'; import { KeyValueBox } from '../nodes/KeyValueBox'; import { ScriptBox } from '../ScriptBox'; import { Templates } from '../Templates'; -import { CollectionSubView } from "./CollectionSubView"; +import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); import { CollectionViewType } from './CollectionView'; import { RichTextField } from '../../../new_fields/RichTextField'; +import { DocumentView } from '../nodes/DocumentView'; export interface TreeViewProps { @@ -45,7 +46,7 @@ export interface TreeViewProps { renderDepth: number; deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; - dropAction: "alias" | "copy" | undefined; + dropAction: dropActionType; addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; @@ -64,6 +65,7 @@ export interface TreeViewProps { treeViewPreventOpen: boolean; renderedIds: string[]; onCheckedClick?: ScriptField; + onChildClick?: ScriptField; ignoreFields?: string[]; } @@ -92,6 +94,7 @@ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); + private _tref = React.createRef<HTMLDivElement>(); get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive @@ -100,7 +103,7 @@ class TreeView extends React.Component<TreeViewProps> { set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } - @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); @@ -128,7 +131,7 @@ class TreeView extends React.Component<TreeViewProps> { } @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => this.props.addDocTab(this.props.containingCollection.childDropAction === "alias" ? Doc.MakeAlias(this.props.document) : this.props.document, "onRight", this.props.libraryPath); + @undoBatch openRight = () => this.props.addDocTab(this.props.dropAction === "alias" ? Doc.MakeAlias(this.props.document) : this.props.document, "onRight", this.props.libraryPath); @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete(); @undoBatch move = (doc: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => { return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); @@ -171,19 +174,29 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline-block"} - editing={this.dataDoc[Id] === EditableView.loadId} + editing={true /*this.dataDoc[Id] === EditableView.loadId*/} contents={StrCast(this.props.document[key])} height={12} fontStyle={style} fontSize={12} GetValue={() => StrCast(this.props.document[key])} - SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} + SetValue={undoBatch((value: string) => { + Doc.SetInPlace(this.props.document, key, value, false) || true; + this.props.document.editTitle = undefined; + })} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - EditableView.loadId = doc[Id]; + const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); + //EditableView.loadId = doc[Id]; + this.props.document.editTitle = undefined; + doc.editTitle = true; return this.props.addDocument(doc); })} + onClick={() => { + SelectionManager.DeselectAll(); + Doc.UserDoc().SelectedDocs = new List([this.props.document]); + return false; + }} OnTab={undoBatch((shift?: boolean) => { EditableView.loadId = this.dataDoc[Id]; shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); @@ -231,7 +244,7 @@ class TreeView extends React.Component<TreeViewProps> { if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceDocument; const destDoc = this.props.document; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree drop link"); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); e.stopPropagation(); } if (de.complete.docDragData) { @@ -254,13 +267,21 @@ class TreeView extends React.Component<TreeViewProps> { docTransform = () => { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); const outerXf = this.props.outerXf(); + const offset = this.props.ScreenToLocalTransform().transformDirection((outerXf.translateX - translateX), outerXf.translateY - translateY); + const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + + return finalXf; + } + getTransform = () => { + const { scale, translateX, translateY } = Utils.GetScreenTransform(this._tref.current!); + const outerXf = this.props.outerXf(); const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)); + const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); return finalXf; } docWidth = () => { const layoutDoc = Doc.Layout(this.props.document); - const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth); + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); 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; } @@ -268,7 +289,7 @@ class TreeView extends React.Component<TreeViewProps> { 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, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); 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) : @@ -296,7 +317,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.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.ignoreFields); + [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields); } else { contentElement = <EditableView key="editableView" @@ -339,7 +360,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.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.ignoreFields)} + [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} </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}> @@ -353,8 +374,10 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} + FreezeDimensions={true} PanelWidth={this.docWidth} PanelHeight={this.docHeight} getTransform={this.docTransform} @@ -390,7 +413,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { 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" : "inherit"), opacity: 0.4 }}> + return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 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>; } @@ -399,8 +422,8 @@ 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[Id], true); + const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); + const editTitle = ScriptField.MakeFunction("this.editTitle=true", { this: Doc.name }); const headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} @@ -419,14 +442,42 @@ class TreeView extends React.Component<TreeViewProps> { <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> </div>); return <> - <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} + <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} onPointerDown={onItemDown} style={{ background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", fontWeight: this.props.document.searchMatch ? "bold" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > - {this.editableView("title")} + {this.props.document.editTitle ? + this.editableView("title") : + <DocumentView + Document={this.props.document} + DataDoc={undefined} + LibraryPath={this.props.libraryPath || []} + addDocument={undefined} + addDocTab={this.props.addDocTab} + rootSelected={returnTrue} + pinToPres={emptyFunction} + onClick={this.props.onChildClick || editTitle} + dropAction={this.props.dropAction} + moveDocument={this.props.moveDocument} + removeDocument={undefined} + ScreenToLocalTransform={this.getTransform} + ContentScaling={returnOne} + PanelWidth={returnZero} + PanelHeight={returnZero} + NativeHeight={returnZero} + NativeWidth={returnZero} + renderDepth={1} + focus={emptyFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + />} </div > {this.props.treeViewHideHeaderFields() ? (null) : headerElements} {openRight} @@ -473,6 +524,7 @@ class TreeView extends React.Component<TreeViewProps> { renderedIds: string[], libraryPath: Doc[] | undefined, onCheckedClick: ScriptField | undefined, + onChildClick: ScriptField | undefined, ignoreFields: string[] | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); @@ -563,6 +615,7 @@ class TreeView extends React.Component<TreeViewProps> { indentDocument={indent} outdentDocument={outdent} onCheckedClick={onCheckedClick} + onChildClick={onChildClick} renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} @@ -586,8 +639,15 @@ class TreeView extends React.Component<TreeViewProps> { } } +export type collectionTreeViewProps = { + treeViewHideTitle?: boolean; + treeViewHideHeaderFields?: boolean; + onCheckedClick?: ScriptField; + onChildClick?: ScriptField; +}; + @observer -export class CollectionTreeView extends CollectionSubView(Document) { +export class CollectionTreeView extends CollectionSubView(Document, undefined as any as collectionTreeViewProps) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; @@ -619,7 +679,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { const doAddDoc = () => Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false); if (this.props.Document.resolvedDataDoc instanceof Promise) { - this.props.Document.resolvedDataDoc.then(resolved => doAddDoc()); + this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc()); } else { doAddDoc(); } @@ -716,18 +776,23 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - const dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; return !childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" - style={{ background: this.props.backgroundColor?.(this.props.Document), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} + style={{ + background: this.props.backgroundColor?.(this.props.Document), + paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`, + paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`, + paddingTop: `${NumCast(this.props.Document._yPadding, 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}> - {(this.props.Document.treeViewHideTitle ? (null) : <EditableView + {(this.props.treeViewHideTitle || this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} editing={false} display={"block"} @@ -746,8 +811,9 @@ export class CollectionTreeView extends CollectionSubView(Document) { { TreeView.GetChildElements(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.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.treeViewHideHeaderFields), - BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick), this.props.ignoreFields) + this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields), + BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick || ScriptCast(this.props.Document.onCheckedClick), + this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields) } </ul> </div > diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index 1c46081a1..b92c5fdd1 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -10,6 +10,59 @@ width: 100%; height: 100%; overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... + + + .collectionTimeView-dragger { + background-color: lightgray; + height: 40px; + width: 20px; + position: absolute; + border-radius: 10px; + top: 55%; + border: 1px black solid; + z-index: 2; + left: -10px; + } + .collectionTimeView-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + position: absolute; + left: 0; + top: 0; + + .collectionTimeView-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: left; + + .collectionTimeView-button { + align-items: center; + display: flex; + width: 100%; + height: 100%; + + .collectionTimeView-span { + margin: auto; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + } + } + + .collectionTimeView-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } + } } #google-tags { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 157229ec8..8192e6751 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,20 +1,19 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx'; +import { action, observable, 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, DataSym } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types'; +import { DataSym, Doc, DocListCast, Field, Opt } from '../../../new_fields/Doc'; +import { List } from '../../../new_fields/List'; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } 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 { Utils, setupMoveUpEvents, returnFalse, returnZero } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; @@ -23,59 +22,49 @@ import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { ScriptBox } from '../ScriptBox'; import { Touchable } from '../Touchable'; +import { CollectionCarouselView } from './CollectionCarouselView'; 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 { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionStaffView } from './CollectionStaffView'; +import { SubCollectionViewProps } from './CollectionSubView'; +import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { CollectionTimeView } from './CollectionTimeView'; -import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; -import { List } from '../../../new_fields/List'; -import { SubCollectionViewProps } from './CollectionSubView'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { listSpec } from '../../../new_fields/Schema'; +import { Docs } from '../../documents/Documents'; +import { ScriptField, ComputedField } from '../../../new_fields/ScriptField'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { ObjectField } from '../../../new_fields/ObjectField'; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); export enum CollectionViewType { - Invalid, - Freeform, - Schema, - Docking, - Tree, - Stacking, - Masonry, - Multicolumn, - Multirow, - Time, - Carousel, - Linear, - Staff -} - -export namespace CollectionViewType { - const stringMapping = new Map<string, CollectionViewType>([ - ["invalid", CollectionViewType.Invalid], - ["freeform", CollectionViewType.Freeform], - ["schema", CollectionViewType.Schema], - ["docking", CollectionViewType.Docking], - ["tree", CollectionViewType.Tree], - ["stacking", CollectionViewType.Stacking], - ["masonry", CollectionViewType.Masonry], - ["multicolumn", CollectionViewType.Multicolumn], - ["multirow", CollectionViewType.Multirow], - ["time", CollectionViewType.Time], - ["carousel", CollectionViewType.Carousel], - ["linear", CollectionViewType.Linear], - ]); - - export const valueOf = (value: string) => stringMapping.get(value.toLowerCase()); + Invalid = "invalid", + Freeform = "freeform", + Schema = "schema", + Docking = "docking", + Tree = 'tree', + Stacking = "stacking", + Masonry = "masonry", + Multicolumn = "multicolumn", + Multirow = "multirow", + Time = "time", + Carousel = "carousel", + Linear = "linear", + Staff = "staff", } export interface CollectionRenderProps { @@ -84,6 +73,7 @@ export interface CollectionRenderProps { moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; + PanelWidth: () => number; } @observer @@ -91,13 +81,16 @@ export class CollectionView extends Touchable<FieldViewProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } private _isChildActive = false; //TODO should this be observable? - @observable private _isLightboxOpen = false; + get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } + set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } @observable private _curLightboxImg = 0; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + get collectionViewType(): CollectionViewType | undefined { - const viewField = NumCast(this.props.Document._viewType); + const viewField = StrCast(this.props.Document._viewType); if (CollectionView._safeMode) { if (viewField === CollectionViewType.Freeform) { return CollectionViewType.Tree; @@ -106,18 +99,20 @@ export class CollectionView extends Touchable<FieldViewProps> { return CollectionViewType.Freeform; } } - return viewField; + return viewField as any as CollectionViewType; } - active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; + active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.rootSelected(outsideReaction) && BoolCast(this.props.Document.forceActive)) || this._isChildActive || this.props.renderDepth === 0; - whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; + whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); @action.bound addDocument(doc: Doc): boolean { const targetDataDoc = this.props.Document[DataSym]; - targetDataDoc[this.props.fieldKey] = new List<Doc>([...DocListCast(targetDataDoc[this.props.fieldKey]), doc]); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there + const docList = DocListCast(targetDataDoc[this.props.fieldKey]); + !docList.includes(doc) && (targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, doc])); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + doc.context = this.props.Document; targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); Doc.GetProto(doc).lastOpened = new DateField; return true; @@ -132,7 +127,8 @@ export class CollectionView extends Touchable<FieldViewProps> { let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); - ContextMenu.Instance.clearItems(); + doc.context = undefined; + ContextMenu.Instance?.clearItems(); if (index !== -1) { value.splice(index, 1); targetDataDoc[this.props.fieldKey] = new List<Doc>(value); @@ -187,7 +183,7 @@ export class CollectionView extends Touchable<FieldViewProps> { 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" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) : - <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; + <CollectionViewBaseChrome CollectionView={this} key="chrome" PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />; return [chrome, this.SubViewHelper(type, renderProps)]; } @@ -233,6 +229,8 @@ export class CollectionView extends Touchable<FieldViewProps> { if (this.props.Document.childDetailed instanceof Doc) { layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, "onRight"), icon: "project-diagram" }); } + layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); + !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); const open = ContextMenu.Instance.findByDescription("Open..."); @@ -241,14 +239,21 @@ export class CollectionView extends Touchable<FieldViewProps> { 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) }); + const funcs = [{ key: "onChildClick", name: "On Child Clicked", script: undefined as any as ScriptField }]; + DocListCast(Cast(Doc.UserDoc().childClickFuncs, Doc, null).data).forEach(childClick => + funcs.push({ key: "onChildClick", name: StrCast(childClick.title), script: ScriptCast(childClick.script) })); + funcs.map(func => onClicks.push({ + description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { + func.script && (this.props.Document[func.key] = ObjectField.MakeCopy(func.script)); + ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name }); + } + })); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, 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" }); - } } @@ -268,6 +273,167 @@ export class CollectionView extends Touchable<FieldViewProps> { onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); } + @observable _facetWidth = 0; + + bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); + getTransform = () => this.props.ScreenToLocalTransform().translate(-this.facetWidth(), 0); + facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); + + @computed get dataDoc() { + return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : + this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template + } + // 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.dataDoc[this.props.fieldKey]; + } + + 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 as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types + } + get childDocList() { + return Cast(this.dataField, listSpec(Doc)); + } + get childDocs() { + const dfield = this.dataField; + const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []); + const docs = rawdocs.filter(d => d && !(d instanceof Promise)).map(d => d as Doc); + const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); + return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + } + @computed get _allFacets() { + const facets = new Set<string>(); + this.childDocs.filter(child => child).forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); + Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.filter(child => child).forEach(child => Object.keys(child).forEach(key => facets.add(key))); + return Array.from(facets); + } + + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const facetCollection = this.props.Document; + const found = DocListCast(facetCollection[this.props.fieldKey + "-filter"]).findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + (facetCollection[this.props.fieldKey + "-filter"] as List<Doc>).splice(found, 1); + const docFilter = Cast(this.props.Document._docFilters, listSpec("string")); + if (docFilter) { + let index: number; + while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { + docFilter.splice(index, 3); + } + } + const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string")); + if (docRangeFilters) { + let index: number; + while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { + docRangeFilters.splice(index, 3); + } + } + } else { + const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); + const facetValues = Array.from(allCollectionDocs.reduce((set, child) => + set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + facetValues.map(val => { + const num = Number(val); + if (Number.isNaN(num)) { + nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + let newFacet: Opt<Doc>; + if (nonNumbers / allCollectionDocs.length < .1) { + newFacet = Docs.Create.SliderDocument({ title: facetHeader }); + const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); + Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox + newFacet.treeViewExpandedView = "layout"; + newFacet.treeViewOpen = true; + newFacet._sliderMin = ranged === undefined ? minVal : ranged[0]; + newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1]; + newFacet._sliderMinThumb = minVal; + newFacet._sliderMaxThumb = maxVal; + newFacet.target = this.props.Document; + const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; + newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); + + Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); + } else { + 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); + } + newFacet && Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); + } + } + + + onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); + return false; + }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0)); + } + filterBackground = () => "dimGray"; + @computed get scriptField() { + const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; + return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + } + @computed get treeIgnoreFields() { return ["_facetCollection", "_docFilters"]; } + @computed get filterView() { + const facetCollection = this.props.Document; + const flyout = ( + <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> + {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> + <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} /> + <span className="checkmark" /> + {facet} + </label>)} + </div> + ); + return !this._facetWidth || this.props.dontRegisterView ? (null) : + <div className="collectionTimeView-treeView" style={{ width: `${this.facetWidth()}px`, overflow: this.facetWidth() < 15 ? "hidden" : undefined }}> + <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> + <div className="collectionTimeView-button"> + <span className="collectionTimeView-span">Facet Filters</span> + <FontAwesomeIcon icon={faEdit} size={"lg"} /> + </div> + </Flyout> + </div> + <div className="collectionTimeView-tree" key="tree"> + <CollectionTreeView {...this.props} + CollectionView={this} + treeViewHideTitle={true} + NativeHeight={returnZero} + NativeWidth={returnZero} + treeViewHideHeaderFields={true} + onCheckedClick={this.scriptField!} + ignoreFields={this.treeIgnoreFields} + annotationsKey={""} + dontRegisterView={true} + PanelWidth={this.facetWidth} + DataDoc={facetCollection} + Document={facetCollection} + backgroundColor={this.filterBackground} + fieldKey={`${this.props.fieldKey}-filter`} + moveDocument={returnFalse} + removeDocument={returnFalse} + addDocument={returnFalse} /> + </div> + </div>; + } + render() { TraceMobx(); const props: CollectionRenderProps = { @@ -276,6 +442,7 @@ export class CollectionView extends Touchable<FieldViewProps> { moveDocument: this.moveDocument, active: this.active, whenActiveChanged: this.whenActiveChanged, + PanelWidth: this.bodyPanelWidth }; return (<div className={"collectionView"} style={{ @@ -285,13 +452,21 @@ export class CollectionView extends Touchable<FieldViewProps> { }} onContextMenu={this.onContextMenu}> {this.showIsTagged()} - {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + <div style={{ width: `calc(100% - ${this.facetWidth()}px)`, marginLeft: `${this.facetWidth()}px` }}> + {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + </div> {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField) ? (Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ? Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} + {!this.props.isSelected() || this.props.PanelHeight() < 100 || this.props.Document.hideFilterView ? (null) : + <div className="collectionTimeView-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> + } + {this.filterView} </div>); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 0c96a662c..a691b4805 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -27,7 +27,6 @@ outline-color: black; border: none; padding: 12px 10px 11px 10px; - margin-left: 50px; } .collectionViewBaseChrome-viewPicker:active { @@ -59,10 +58,10 @@ position: absolute; width: 40px; transform-origin: top left; + pointer-events: all; // margin-top: 10px; } .collectionViewBaseChrome-template { - margin-left: 10px; display: grid; background: rgb(238, 238, 238); color:grey; @@ -169,23 +168,26 @@ } - .collectionStackingViewChrome-sectionFilter-cont, - .collectionTreeViewChrome-sectionFilter-cont { + .collectionStackingViewChrome-pivotField-cont, + .collectionTreeViewChrome-pivotField-cont { justify-self: right; display: flex; font-size: 75%; letter-spacing: 2px; - .collectionStackingViewChrome-sectionFilter-label, - .collectionTreeViewChrome-sectionFilter-label { + .collectionStackingViewChrome-pivotField-label, + .collectionTreeViewChrome-pivotField-label { vertical-align: center; - padding: 10px; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 10px; } - .collectionStackingViewChrome-sectionFilter, - .collectionTreeViewChrome-sectionFilter { + .collectionStackingViewChrome-pivotField, + .collectionTreeViewChrome-pivotField { color: white; - width: 100px; + width:100%; + min-width: 100px; text-align: center; background: rgb(238, 238, 238); @@ -210,8 +212,8 @@ } } - .collectionStackingViewChrome-sectionFilter:hover, - .collectionTreeViewChrome-sectionFilter:hover { + .collectionStackingViewChrome-pivotField:hover, + .collectionTreeViewChrome-pivotField:hover { cursor: text; } } @@ -278,7 +280,7 @@ 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 49a9e8200..b2ca3c19f 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -8,7 +8,7 @@ import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils, emptyFunction } from "../../../Utils"; +import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; @@ -24,6 +24,7 @@ interface CollectionViewChromeProps { CollectionView: CollectionView; type: CollectionViewType; collapse?: (value: boolean) => any; + PanelWidth: () => number; } interface Filter { @@ -38,25 +39,30 @@ const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) + get target() { return this.props.CollectionView.props.Document; } _templateCommand = { - title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"], + params: ["target", "source"], title: "=> item view", + script: "this.target.childLayout = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]), 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"], + params: ["target", "source"], title: "=> click item view", + script: "this.target.childDetailed = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childDetailed = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, - immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; _contentCommand = { - title: "=> content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], + params: ["target", "source"], title: "=> content", + script: "getProto(this.target).data = copyField(this.source);", + immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(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: "=> 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; }, + params: ["target"], title: "=> saved view", + script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", + immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }, + initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; }, }; _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; @@ -77,6 +83,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } private _picker: any; private _commandRef = React.createRef<HTMLInputElement>(); + private _viewRef = React.createRef<HTMLInputElement>(); private _autosuggestRef = React.createRef<Autosuggest>(); @observable private _currentKey: string = ""; @observable private _viewSpecsOpen: boolean = false; @@ -151,7 +158,7 @@ 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 = e.target.selectedOptions[0].value; } commandChanged = (e: React.ChangeEvent) => { @@ -260,10 +267,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; if (collapsed) return null; switch (this.props.type) { - case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); default: return null; } } @@ -339,8 +346,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; private _sensitivity: number = 16; + dragViewDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + const vtype = this.props.CollectionView.collectionViewType; + const c = { + params: ["target"], title: vtype, + script: `this.target._viewType = ${StrCast(this.props.CollectionView.props.Document._viewType)}`, + immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]), + initialize: emptyFunction, + }; + DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), + { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY); + return true; + }, emptyFunction, emptyFunction); + } dragCommandDown = (e: React.PointerEvent) => { - this._startDragPosition = { x: e.clientX, y: e.clientY }; document.addEventListener("pointermove", this.dragPointerMove); document.addEventListener("pointerup", this.dragPointerUp); @@ -363,41 +383,52 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro dragPointerUp = (e: PointerEvent) => { document.removeEventListener("pointermove", this.dragPointerMove); document.removeEventListener("pointerup", this.dragPointerUp); - } render() { 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" style={{ border: "unset" }}> + <div className="collectionViewChrome-cont" style={{ + top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined, + transform: collapsed ? "" : `scale(${Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)})`, + transformOrigin: "top left", + width: `${this.props.PanelWidth() / Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)}px` + }}> + <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" style={{ top: collapsed ? 70 : 10, - transform: `rotate(${collapsed ? 180 : 0}deg) scale(${collapsed ? 0.5 : 1}) translate(${collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: (collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9, + transform: `rotate(${collapsed ? 180 : 0}deg) scale(0.5) translate(${collapsed ? "-100%, -100%" : "0, 0"})`, + opacity: 0.9, + display: (collapsed && !this.props.CollectionView.props.isSelected()) ? "none" : undefined, left: (collapsed ? 0 : "unset"), }} title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> - <select - className="collectionViewBaseChrome-viewPicker" - onPointerDown={stopPropagation} - onChange={this.viewChanged} - style={{ display: collapsed ? "none" : undefined }} - 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">MultiCol</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">MultiRow</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Pivot/Time</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Carousel</option> - </select> + <div className="collectionViewBaseChrome-template" style={{ marginLeft: 25, display: collapsed ? "none" : undefined }}> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> + </div> + <select + className="collectionViewBaseChrome-viewPicker" + onPointerDown={stopPropagation} + onChange={this.viewChanged} + value={StrCast(this.props.CollectionView.props.Document._viewType)}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="freeform">Freeform</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="schema">Schema</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="tree">Tree</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="stacking">Stacking</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="masonry">Masonry</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multicolumn">MultiCol</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multirow">MultiRow</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="time">Pivot/Time</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="carousel">Carousel</option> + </select> + </div> + </div> <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" /> @@ -444,7 +475,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> <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> + <FontAwesomeIcon icon="bullseye" size="2x" /> </div> <select className="collectionViewBaseChrome-cmdPicker" @@ -472,7 +503,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView @observable private suggestions: string[] = []; @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); } - @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); } + @computed get pivotField() { return StrCast(this.props.CollectionView.props.Document._pivotField); } getKeySuggestions = async (value: string): Promise<string[]> => { value = value.toLowerCase(); @@ -510,26 +541,26 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView } setValue = (value: string) => { - this.props.CollectionView.props.Document.sectionFilter = value; + this.props.CollectionView.props.Document._pivotField = value; return true; } @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }; - @action resetValue = () => { this._currentKey = this.sectionFilter; }; + @action resetValue = () => { this._currentKey = this.pivotField; }; render() { return ( <div className="collectionStackingViewChrome-cont"> - <div className="collectionStackingViewChrome-sectionFilter-cont"> - <div className="collectionStackingViewChrome-sectionFilter-label"> - GROUP ITEMS BY: + <div className="collectionStackingViewChrome-pivotField-cont"> + <div className="collectionStackingViewChrome-pivotField-label"> + GROUP BY: </div> <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> </div> - <div className="collectionStackingViewChrome-sectionFilter"> + <div className="collectionStackingViewChrome-pivotField"> <EditableView - GetValue={() => this.sectionFilter} + GetValue={() => this.pivotField} autosuggestProps={ { resetValue: this.resetValue, @@ -551,7 +582,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView }} oneLine SetValue={this.setValue} - contents={this.sectionFilter ? this.sectionFilter : "N/A"} + contents={this.pivotField ? this.pivotField : "N/A"} /> </div> </div> diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 43ba5c614..afc1c6754 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import './ParentDocumentSelector.scss'; import { Doc } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; -import { observable, action, runInAction } from "mobx"; +import { observable, action, runInAction, trace, computed } from "mobx"; import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; @@ -14,6 +14,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; import { DocumentView } from "../nodes/DocumentView"; +import { SelectionManager } from "../../util/SelectionManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -22,7 +23,6 @@ library.add(faCog); type SelectorProps = { Document: Doc, - Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, location: string): void }; @@ -54,7 +54,7 @@ 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) { + if (col._viewType === CollectionViewType.Freeform) { const newPanX = NumCast(target.x) + NumCast(target._width) / 2; const newPanY = NumCast(target.y) + NumCast(target._height) / 2; col._panX = newPanX; @@ -93,9 +93,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> { } @observer -export class DockingViewButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { - @observable hover = false; - +export class DockingViewButtonSelector extends React.Component<{ views: DocumentView[], Stack: any }> { customStylesheet(styles: any) { return { ...styles, @@ -105,16 +103,19 @@ export class DockingViewButtonSelector extends React.Component<{ Document: Doc, }, }; } + _ref = React.createRef<HTMLDivElement>(); - render() { - const view = DocumentManager.Instance.getDocumentView(this.props.Document); - const flyout = ( - <div className="ParentDocumentSelector-flyout" title=" "> - <DocumentButtonBar views={[view]} stack={this.props.Stack} /> + @computed get flyout() { + return ( + <div className="ParentDocumentSelector-flyout" title=" " ref={this._ref}> + <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> </div> ); - return <span title="Tap for menu, drag tab as document" onPointerDown={e => !this.props.Stack && e.stopPropagation()} className="buttonSelector"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} stylesheet={this.customStylesheet}> + } + + render() { + return <span title="Tap for menu, drag tab as document" onPointerDown={e => { if (getComputedStyle(this._ref.current!).width !== "100%") {e.stopPropagation();e.preventDefault();} this.props.views[0].select(false); }} className="buttonSelector"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={"cog"} size={"sm"} /> </Flyout> </span>; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 637c81fe2..bd4db89ec 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -147,6 +147,7 @@ export function computePivotLayout( const expander = 1.05; const gap = .15; + const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols); let x = 0; const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort(); sortedPivotKeys.forEach(key => { @@ -189,7 +190,6 @@ export function computePivotLayout( x += pivotAxisWidth * (numCols * expander + gap); }); - const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols); const dividers = sortedPivotKeys.map((key, i) => ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters })); groupNames.push(...dividers); @@ -348,7 +348,7 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { y: gname.y * scale, color: gname.color, width: gname.width === undefined ? undefined : gname.width * scale, - height: gname.height === -1 ? 1 : Math.max(fontHeight, (gname.height || 0) * scale), + height: gname.height === -1 ? 1 : gname.type === "text" ? Math.max(fontHeight * scale, (gname.height || 0) * scale) : (gname.height || 0) * scale, fontSize: gname.fontSize, payload: gname.payload }))); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 1038347d4..09fc5148e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -26,8 +26,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo action(() => { setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => this._opacity = 0.05), 750); // this will unhighlight the link line. - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); const a = adiv.getBoundingClientRect(); @@ -43,11 +43,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; - // really hacky stuff to make the DocuLinkBox display where we want it to: + // really hacky stuff to make the LinkAnchorBox display where we want it to: // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = window.document.getElementById((this.props.LinkDocs[0][afield] as Doc)[Id]); - const targetBhyperlink = window.document.getElementById((this.props.LinkDocs[0][bfield] as Doc)[Id]); + const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]); + const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]); if (!targetBhyperlink) { this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; @@ -81,8 +81,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } render() { - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 044d35eca..d12f93f15 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed, IReactionDisposer } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; @@ -7,69 +7,12 @@ import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { Utils } from "../../../../Utils"; +import { Utils, emptyFunction } from "../../../../Utils"; import { SelectionManager } from "../../../util/SelectionManager"; import { DocumentType } from "../../../documents/DocumentTypes"; @observer export class CollectionFreeFormLinksView extends React.Component { - - _brushReactionDisposer?: IReactionDisposer; - componentDidMount() { - // this._brushReactionDisposer = reaction( - // () => { - // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; - // }, - // () => { - // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; - // views.forEach((dstDoc, i) => { - // views.forEach((srcDoc, j) => { - // let dstTarg = dstDoc; - // let srcTarg = srcDoc; - // let x1 = NumCast(srcDoc.x); - // let x2 = NumCast(dstDoc.x); - // let x1w = NumCast(srcDoc.width, -1); - // let x2w = NumCast(dstDoc.width, -1); - // if (x1w < 0 || x2w < 0 || i === j) { } - // else { - // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { - // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; - // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; - // }); - // let brushAction = (field: (Doc | Promise<Doc>)[]) => { - // let found = findBrush(field); - // if (found !== -1) { - // field.splice(found, 1); - // } - // }; - // if (Math.abs(x1 + x1w - x2) < 20) { - // let linkDoc: Doc = new Doc(); - // linkDoc.title = "Histogram Brush"; - // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); - // linkDoc.brushingDocs = new List([dstTarg, srcTarg]); - - // brushAction = (field: (Doc | Promise<Doc>)[]) => { - // if (findBrush(field) === -1) { - // field.push(linkDoc); - // } - // }; - // } - // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>(); - // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>(); - // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); - // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); - // brushAction(dstBrushDocs); - // brushAction(srcBrushDocs); - // } - // }); - // }); - // }); - } - componentWillUnmount() { - this._brushReactionDisposer && this._brushReactionDisposer(); - } @computed get uniqueConnections() { const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { @@ -86,8 +29,10 @@ export class CollectionFreeFormLinksView extends React.Component { } return drawnPairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); - return connections.filter(c => c.a.props.Document.type === DocumentType.LINK) // get rid of the filter to show links to documents in addition to document anchors - .map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); + return connections.filter(c => + c.a.props.layoutKey && c.b.props.layoutKey && c.a.props.Document.type === DocumentType.LINK && + c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed + ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 0b5e44ccb..730392ab5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -36,6 +36,7 @@ height: 100%; display: flex; align-items: center; + .collectionfreeformview-placeholderSpan { font-size: 32; display: flex; @@ -99,4 +100,10 @@ #prevCursor { animation: blink 1s infinite; +} + +.pullpane-indicator { + z-index: 99999; + background-color: rgba($color: #000000, $alpha: .4); + position: absolute; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ca8d5e18b..146ec9f7d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,24 +1,26 @@ import { library } from "@fortawesome/fontawesome-svg-core"; 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, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../../new_fields/Doc"; +import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; -import { InkTool } from "../../../../new_fields/InkField"; +import { InkData, InkField, InkTool } from "../../../../new_fields/InkField"; +import { List } from "../../../../new_fields/List"; +import { RichTextField } from "../../../../new_fields/RichTextField"; import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast, ScriptCast, BoolCast, StrCast } from "../../../../new_fields/Types"; +import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; -import { aggregateBounds, intersectRect, returnOne, Utils } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils, returnZero } from "../../../../Utils"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager } from "../../../util/DragManager"; +import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -33,6 +35,7 @@ import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; +import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; @@ -63,16 +66,24 @@ export const panZoomSchema = createSchema({ type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); +export type collectionFreeformViewProps = { + forceScaling?:boolean; // whether to force scaling of content (needed by ImageBox) +}; @observer -export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { +export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, undefined as any as collectionFreeformViewProps) { 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 = observable.map<string, any>(); + private _layoutPoolData = new ObservableMap<string, any>(); private _cachedPool: Map<string, any> = new Map(); + @observable private _pullCoords: number[] = [0, 0]; + @observable private _pullDirection: string = ""; 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 @@ -80,9 +91,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @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!), 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); } + @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.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); } + @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.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"; @@ -91,6 +102,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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) + private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); @@ -322,31 +334,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this._lastX = e.pageX; this._lastY = e.pageY; } - // eraser or scrubber plus anything else mode + // eraser plus anything else mode else { e.stopPropagation(); e.preventDefault(); } } - // if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { - // document.removeEventListener("pointermove", this.onPointerMove); - // document.removeEventListener("pointerup", this.onPointerUp); - // document.addEventListener("pointermove", this.onPointerMove); - // document.addEventListener("pointerup", this.onPointerUp); - // if (InkingControl.Instance.selectedTool === InkTool.None) { - // this._lastX = e.pageX; - // this._lastY = e.pageY; - // } - // else { - // e.stopPropagation(); - // e.preventDefault(); - - // if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - // let point = this.getTransform().transformPoint(e.pageX, e.pageY); - // this._points.push({ x: point[0], y: point[1] }); - // } - // } - // } } @action @@ -411,8 +404,91 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); 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)); + const strokes: InkData[] = []; + inks.forEach(i => { + const d = Cast(i.data, InkField); + const x = NumCast(i.x); + const y = NumCast(i.y); + const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); + const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); + if (d) { + strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); + } + }); + + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { + console.log(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 if (word.alternates) { + 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(); + } } } @@ -428,37 +504,36 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { - // I think it makes sense for the marquee menu to go away when panned. -syip2 - MarqueeOptionsMenu.Instance.fadeOut(true); + // bcz: theres should be a better way of doing these than referencing these static instances directly + MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 + PDFMenu.Instance.fadeOut(true); - let x = this.Document._panX || 0; - let y = this.Document._panY || 0; - const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) { - PDFMenu.Instance.fadeOut(true); - const minx = this.childDataProvider(docs[0]).x; - const miny = this.childDataProvider(docs[0]).y; - const maxx = this.childDataProvider(docs[0]).width + minx; - const maxy = this.childDataProvider(docs[0]).height + miny; - const ranges = docs.filter(doc => doc && this.childDataProvider(doc)).reduce((range, doc) => { - const x = this.childDataProvider(doc).x; - const y = this.childDataProvider(doc).y; - const xe = this.childDataProvider(doc).width + x; - const ye = this.childDataProvider(doc).height + y; - 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]]); - - const cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; - const panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, - this.props.PanelHeight() / this.zoomScaling() * cscale); - if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; - if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; - if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; - if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + let x = (this.Document._panX || 0) - dx; + let y = (this.Document._panY || 0) - dy; + if (!this.isAnnotationOverlay) { + // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds + const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); + const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc)).map(doc => this.childDataProvider(doc)); + if (measuredDocs.length) { + const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content + ({ + xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) }, + yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) } + }) + , { + xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, + yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } + }); + + const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()]; + if (ranges.xrange.min > (this.panX() + panelDim[0] / 2)) x = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds + if (ranges.xrange.max < (this.panX() - panelDim[0] / 2)) x = ranges.xrange.min - panelDim[0] / 2; + if (ranges.yrange.min > (this.panY() + panelDim[1] / 2)) y = ranges.yrange.max + panelDim[1] / 2; + if (ranges.yrange.max < (this.panY() - panelDim[1] / 2)) y = ranges.yrange.min - panelDim[1] / 2; + } } - this.setPan(x - dx, y - dy); + this.setPan(x, y); this._lastX = e.clientX; this._lastY = e.clientY; } @@ -547,7 +622,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // use the centerx and centery as the "new mouse position" 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.pan({ clientX: centerX, clientY: centerY }); + // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY); + + if (!this._pullDirection) { // if we are not bezel movement + this.pan({ clientX: centerX, clientY: centerY }); + } else { + this._pullCoords = [centerX, centerY]; + } + this._lastX = centerX; this._lastY = centerY; } @@ -572,6 +654,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this._lastX = centerX; this._lastY = centerY; + const screenBox = this._mainCont?.getBoundingClientRect(); + + + // determine if we are using a bezel movement + if (screenBox) { + if ((screenBox.right - centerX) < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "right"; + } else if (centerX - screenBox.left < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "left"; + } else if (screenBox.bottom - centerY < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "bottom"; + } else if (centerY - screenBox.top < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "top"; + } + } + + this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -582,12 +685,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } cleanUpInteractions = () => { + switch (this._pullDirection) { + case "left": + case "right": + case "top": + case "bottom": + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), this._pullDirection); + } + + this._pullDirection = ""; + this._pullCoords = [0, 0]; + document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); } + @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1; @@ -670,6 +785,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const offset = annotOn && (contextHgt / 2 * 96 / 72); this.props.Document.scrollY = NumCast(doc.y) - offset; } + + afterFocus && setTimeout(afterFocus, 1000); } else { const layoutdoc = Doc.Layout(doc); const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; @@ -680,14 +797,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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 + if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { + 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); this.props.focus(this.props.Document); willZoom && this.setScaleToZoom(layoutdoc, scale); Doc.linkFollowHighlight(doc); afterFocus && setTimeout(() => { - if (afterFocus && afterFocus()) { + if (afterFocus?.()) { this.Document._panX = savedState.px; this.Document._panY = savedState.py; this.Document.scale = savedState.s; @@ -702,12 +821,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } - zoomToScale = (scale: number) => { - this.Document.scale = scale; - } - - 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); } backgroundHalo = () => BoolCast(this.Document.useClusters); @@ -715,10 +828,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { ...this.props, + NativeHeight: returnZero, + NativeWidth: returnZero, + fitToBox: false, DataDoc: childData, Document: childLayout, LibraryPath: this.libraryPath, + FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, + rootSelected: this.rootSelected, + dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, //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, @@ -733,11 +852,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { backgroundHalo: this.backgroundHalo, parentActive: this.props.active, bringToFront: this.bringToFront, - zoomToScale: this.zoomToScale, - getScale: this.getScale + addDocTab: this.addDocTab, }; } + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const result = this.Document.arrangeScript?.script.run(params, console.log); if (result?.success) { @@ -789,12 +914,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }.bind(this)); doTimelineLayout(poolData: Map<string, PoolData>) { - return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.childDocs, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } doPivotLayout(poolData: Map<string, PoolData>) { - return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + return computePivotLayout(poolData, this.props.Document, this.childDocs, this.childDocs, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } @@ -820,42 +945,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } - @computed get filterDocs() { - const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []); - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - } - const filteredDocs = docFilters.length ? this.childDocs.filter(d => { - for (const facetKey of Object.keys(filterFacets)) { - const facet = filterFacets[facetKey]; - const satisfiesFacet = Object.keys(facet).some(value => - (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value)); - if (!satisfiesFacet) { - return false; - } - } - return true; - }) : this.childDocs; - const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); - const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { - return false; - } - } - return true; - }); - return rangeFilteredDocs; - } childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); get doLayoutComputation() { const { newPool, computedElementData } = this.doInternalLayoutComputation; @@ -872,11 +961,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const elements: ViewDefResult[] = computedElementData.slice(); this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} + ele: <CollectionFreeFormDocumentView + key={pair.layout[Id]} + {...this.getChildDocumentViewProps(pair.layout, pair.data)} dataProvider={this.childDataProvider} LayoutDoc={this.childLayoutDocFunc} jitterRotation={NumCast(this.props.Document.jitterRotation)} - fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />, + fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.freezeChildDimensions)} + />, bounds: this.childDataProvider(pair.layout) })); @@ -995,7 +1088,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (doc instanceof Doc) { const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); doc.x = xx, doc.y = yy; - this.props.addDocument && this.props.addDocument(doc); + this.props.addDocument?.(doc); } } } @@ -1035,14 +1128,18 @@ 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; + if (this.props.annotationsKey && !this.props.forceScaling) return 0; + const nw = NumCast(this.Document._nativeWidth, this.props.NativeWidth()); + const nh = NumCast(this.Document._nativeHeight, this.props.NativeHeight()); + const hscale = nh ? this.props.PanelHeight() / nh : 1; + const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } render() { TraceMobx(); + const clientRect = this._mainCont?.getBoundingClientRect(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) // this.Document.fitX = this.contentBounds && this.contentBounds.x; // this.Document.fitY = this.contentBounds && this.contentBounds.y; @@ -1064,7 +1161,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> - </div>; + + <div className={"pullpane-indicator"} + style={{ + display: this._pullDirection ? "block" : "none", + top: clientRect ? this._pullDirection === "bottom" ? this._pullCoords[1] - clientRect.y : 0 : "auto", + // left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x - MainView.Instance.flyoutWidth : 0 : "auto", + left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x : 0 : "auto", + width: clientRect ? this._pullDirection === "left" ? this._pullCoords[0] - clientRect.left : this._pullDirection === "right" ? clientRect.right - this._pullCoords[0] : clientRect.width : 0, + height: clientRect ? this._pullDirection === "top" ? this._pullCoords[1] - clientRect.top : this._pullDirection === "bottom" ? clientRect.bottom - this._pullCoords[1] : clientRect.height : 0, + + }}> + </div> + + </div >; } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 71f265484..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; @@ -43,6 +44,13 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { 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 d4fa22a7e..503df10c2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,11 +1,10 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../../new_fields/Doc"; -import { InkField } from "../../../../new_fields/InkField"; +import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { InkField, InkData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { Cast, NumCast } from "../../../../new_fields/Types"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; +import { Cast, NumCast, FieldValue, StrCast } from "../../../../new_fields/Types"; import { Utils } from "../../../../Utils"; import { Docs, DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -17,6 +16,8 @@ import { SubCollectionViewProps } from "../CollectionSubView"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { RichTextField } from "../../../../new_fields/RichTextField"; import { CollectionView } from "../CollectionView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; @@ -105,13 +106,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); } else if (!e.ctrlKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; - this.props.addLiveTextDocument( - Docs.Create.TextDocument("", { _width: NumCast((FormattedTextBox.DefaultLayout as Doc)?._width) || 200, _height: 100, layout: FormattedTextBox.DefaultLayout, 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-" }); - text.layout = notes[(e.keyCode - 49) % notes.length]; - this.props.addLiveTextDocument(text); + const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); + const template = FormattedTextBox.DefaultLayout; + if (template instanceof Doc) { + tbox._width = NumCast(template._width); + tbox.layoutKey = "layout_" + StrCast(template.title); + tbox[StrCast(tbox.layoutKey)] = template; + } + this.props.addLiveTextDocument(tbox); } e.stopPropagation(); } @@ -209,6 +211,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); @@ -336,7 +339,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.displayTimecode = undefined; + d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection return d; }); } @@ -348,6 +351,85 @@ 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 strokes: InkData[] = []; + inks.forEach(i => { + const d = Cast(i.data, InkField); + const x = NumCast(i.x); + const y = NumCast(i.y); + const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); + const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); + if (d) { + strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); + } + }); + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).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; + // } + // } + // } + // }) + // } + // 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(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); + // inks[i].alternativeColors = new List<string>(uniqueColors); + // if (wordToColor.has(word.recognizedText.toLowerCase())) { + // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); + // } + // else if (word.alternates) { + // for (const alt of word.alternates) { + // if (wordToColor.has(alt.recognizedString.toLowerCase())) { + // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); + // break; + // } + // } + // } + // }); + // } + const lines = results.filter((r: any) => r.category === "line"); + console.log(lines); + const text = lines.map((l: any) => l.recognizedText).join("\r\n"); + this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); + }); + } + } + + @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -358,13 +440,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "-summary-" }); + const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); const portal = Doc.MakeAlias(summary); - Doc.GetProto(summary)["data-annotations"] = new List<Doc>(selected); - Doc.GetProto(summary).layout_portal = CollectionView.LayoutString("data-annotations"); + Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected); + Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations"); summary._backgroundColor = "#e2ad32"; portal.layoutKey = "layout_portal"; - DocUtils.MakeLink({ doc: summary, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal link", "portal link"); + portal.title = "document collection"; + DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing"); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 82175c0b5..7e511ae34 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -14,6 +14,7 @@ import "./collectionMulticolumnView.scss"; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; import { List } from '../../../../new_fields/List'; +import { returnZero } from '../../../../Utils'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; const MulticolumnDocument = makeInterface(documentSchema); @@ -208,6 +209,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} @@ -223,17 +228,13 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get contents(): JSX.Element[] | null { - // bcz: feels like a hack ... trying to show something useful when there's no list document in the data field of a templated object - const expanded = Cast(this.props.Document.expandedTemplate, Doc, null); - let { childLayoutPairs } = this.dataDoc[this.props.fieldKey] instanceof List || !expanded ? this : { childLayoutPairs: [] } as { childLayoutPairs: { layout: Doc, data: Doc }[] }; - const replaced = !childLayoutPairs.length && !Cast(expanded?.layout, Doc, null) && expanded; - childLayoutPairs = childLayoutPairs.length || !replaced ? childLayoutPairs : [{ layout: replaced, data: replaced }]; + const { childLayoutPairs } = this; const { Document, PanelHeight } = this.props; const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)); - const width = () => expanded ? this.props.PanelWidth() : this.lookupPixels(layout); + const width = () => this.lookupPixels(layout); const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( <div className={"document-wrapper"} @@ -248,6 +249,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu <ResizeBar width={resizerWidth} key={"resizer" + i} + select={this.props.select} columnUnitLength={this.getColumnUnitLength} toLeft={layout} toRight={childLayoutPairs[i + 1]?.layout} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 5e59f8237..daf1fda6c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -6,7 +6,7 @@ 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 { Utils, returnZero } from '../../../../Utils'; import "./collectionMultirowView.scss"; import { computed, trace, observable, action } from 'mobx'; import { Transform } from '../../../util/Transform'; @@ -208,6 +208,10 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {...this.props} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} + FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} CollectionDoc={this.props.Document} PanelWidth={width} diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 2cbeb3526..e1e604686 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -11,6 +11,7 @@ interface ResizerProps { columnUnitLength(): number | undefined; toLeft?: Doc; toRight?: Doc; + select: (isCtrlPressed: boolean) => void; } const resizerOpacity = 1; @@ -23,6 +24,7 @@ export default class ResizeBar extends React.Component<ResizerProps> { @action private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { + this.props.select(false); e.stopPropagation(); e.preventDefault(); window.removeEventListener("pointermove", this.onPointerMove); |
