diff options
Diffstat (limited to 'src/client/views/collections')
39 files changed, 2204 insertions, 1196 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 4815f1a59..fd1296286 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,6 +1,7 @@ .collectionCarouselView-outer { background: gray; + height : 100%; .collectionCarouselView-caption { margin-left: 10%; margin-right: 10%; @@ -14,27 +15,23 @@ width: 100%; } } -.carouselView-back { +.carouselView-back, .carouselView-fwd { position: absolute; display: flex; - left: 0; top: 50%; width: 30; height: 30; - background: lightgray; align-items: center; border-radius: 5px; justify-content: center; + background : rgba(255, 255, 255, 0.46); } -.carouselView-fwd { - position: absolute; - display: flex; +.carouselView-fwd { right: 0; - top: 50%; - width: 30; - height: 30; +} +.carouselView-back { + left: 0; +} +.carouselView-back:hover, .carouselView-fwd:hover { background: lightgray; - align-items: center; - border-radius: 5px; - justify-content: center; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 0933d5924..a0cb1fe19 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -12,28 +12,22 @@ import { CollectionSubView } from './CollectionSubView'; import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { Doc } from '../../../new_fields/Doc'; import { FormattedTextBox } from '../nodes/FormattedTextBox'; - - - +import { ContextMenu } from '../ContextMenu'; +import { ObjectField } from '../../../new_fields/ObjectField'; type CarouselDocument = makeInterface<[typeof documentSchema,]>; const CarouselDocument = makeInterface(documentSchema); @observer export class CollectionCarouselView extends CollectionSubView(CarouselDocument) { - @observable public addMenuToggle = React.createRef<HTMLInputElement>(); private _dropDisposer?: DragManager.DragDropDisposer; - componentWillUnmount() { - this._dropDisposer && this._dropDisposer(); - } + componentWillUnmount() { this._dropDisposer?.(); } - componentDidMount() { - } protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this._dropDisposer && this._dropDisposer(); + this._dropDisposer?.(); if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); } } @@ -50,18 +44,18 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : - <div> - <div className="collectionCarouselView-image"> + <> + <div className="collectionCarouselView-image" key="image"> <ContentFittingDocumentView {...this.props} Document={this.childLayoutPairs[index].layout} DataDocument={this.childLayoutPairs[index].data} PanelHeight={this.panelHeight} getTransform={this.props.ScreenToLocalTransform} /> </div> - <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}> + <div className="collectionCarouselView-caption" key="caption" style={{ background: this.props.backgroundColor?.(this.props.Document) }}> <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox> </div> - </div> + </>; } @computed get buttons() { return <> @@ -73,8 +67,21 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) </div> </>; } + + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + ContextMenu.Instance.addItem({ + description: "Make Hero Image", event: () => { + const index = NumCast(this.layoutDoc._itemIndex); + (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField); + }, icon: "plus" + }); + } + } render() { - return <div className="collectionCarouselView-outer"> + return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> {this.content} {this.buttons} </div>; diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index f518ef8fb..2fafcecb2 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,8 +1,34 @@ @import "../../views/globalCssVariables.scss"; -.lm_active .messageCounter { - color: white; - background: #999999; +.lm_title { + margin-top: 3px; + background: black; + border-radius: 5px; + border: solid 1px dimgray; + border-width: 2px 2px 0px; + height: 20px; + transform: translate(0px, -3px); +} +.lm_title_wrap { + overflow: hidden; + height: 19px; + margin-top: -3px; + display:inline-block; +} +.lm_active .lm_title { + border: solid 1px lightgray; +} +.lm_header .lm_tab .lm_close_tab { + position: absolute; + text-align: center; +} + +.lm_header .lm_tab { + padding-right : 20px; +} + +.lm_popout { + display:none; } .messageCounter { @@ -26,9 +52,20 @@ top: 0; left: 0; // overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu) - + .collectionDockingView-gear { + padding-left: 5px; + height: 15px; + width: 18px; + display: inline-block; + margin: auto; + } .collectionDockingView-dragAsDocument { touch-action: none; + position: absolute; + padding-left: 5px; + display: inline-block; + width: 100%; + height: 100%; } .lm_content { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 7a6d54ac2..b85cc9b56 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,26 +1,27 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faFile } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, Lambda, observable, reaction, computed, runInAction, trace } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Field, Opt, DataSym } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { FieldId } from "../../../new_fields/RefField"; -import { listSpec } from "../../../new_fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +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, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils } 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 { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from "../../util/UndoManager"; @@ -28,13 +29,8 @@ import { MainView } from '../MainView'; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; +import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); -import { ButtonSelector } from './ParentDocumentSelector'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ComputedField } from '../../../new_fields/ScriptField'; -import { InteractionUtils } from '../../util/InteractionUtils'; -import { TraceMobx } from '../../../new_fields/util'; -import { Scripting } from '../../util/Scripting'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -42,7 +38,7 @@ const _global = (window /* browser */ || global /* node */) as any; export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @observable public static Instances: CollectionDockingView[] = []; @computed public static get Instance() { return CollectionDockingView.Instances[0]; } - public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number, libraryPath?: Doc[]) { + public static makeDocumentConfig(document: Doc, width?: number, libraryPath?: Doc[]) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -50,8 +46,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : "", - libraryPath: libraryPath ? libraryPath.map(d => d[Id]) : [] + libraryPath: libraryPath?.map(d => d[Id]) //collectionDockingView: CollectionDockingView.Instance } }; @@ -80,12 +75,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp public StartOtherDrag(e: any, dragDocs: Doc[]) { let config: any; if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined); + config = CollectionDockingView.makeDocumentConfig(dragDocs[0]); } else { config = { type: 'row', content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc, undefined); + CollectionDockingView.makeDocumentConfig(doc); }) }; } @@ -101,10 +96,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { const document = Doc.MakeAlias(docView.props.Document); - const dataDoc = docView.props.DataDoc; const newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] + content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] }; const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); @@ -176,35 +170,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } @undoBatch @action - public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean { - if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] - }; - - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - + public static ReplaceRightSplit(document: Doc, libraryPath?: Doc[], addToSplit?: boolean): 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.isDisplayPanle) { - child.contentItems[0].remove(); - child.addChild(newContentItem, undefined, true); + DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) { + const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); + child.addChild(newItemStackConfig, undefined); + !addToSplit && child.contentItems[0].remove(); instance.layoutChanged(document); return true; - } else { - Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { - child.contentItems[j].remove(); - child.addChild(newContentItem, undefined, true); - return true; - } - return false; - }); } - return false; + return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { + const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); + child.addChild(newItemStackConfig, undefined); + !addToSplit && child.contentItems[j].remove(); + instance.layoutChanged(document); + return true; + } + return false; + }); }); } if (retVal) { @@ -219,12 +208,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // @undoBatch @action - public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { + public static AddRightSplit(document: Doc, libraryPath?: Doc[]) { if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; const newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] + content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] }; const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); @@ -323,18 +312,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // @undoBatch @action - public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { + public static UseRightSplit(document: Doc, libraryPath?: Doc[], shiftKey?: boolean) { document.isDisplayPanel = true; - if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) { - CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath); + if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, libraryPath, shiftKey)) { + CollectionDockingView.AddRightSplit(document, libraryPath); } } @undoBatch @action - public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => { + public AddTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument, undefined, libraryPath); + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); if (stack === undefined) { let stack: any = this._goldenLayout.root; while (!stack.isStack) { @@ -395,7 +384,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp componentDidMount: () => void = () => { if (this._containerRef.current) { this.reactionDisposer = reaction( - () => StrCast(this.props.Document.dockingConfig), + () => this.props.Document.dockingConfig, () => { if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { // Because this is in a set timeout, if this component unmounts right after mounting, @@ -457,16 +446,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp }); window.addEventListener("pointerup", onPointerUp); const className = (e.target as any).className; - if (className === "messageCounter") { - e.stopPropagation(); - e.preventDefault(); - const x = e.clientX; - const y = e.clientY; - const docid = (e.target as any).DashDocId; - const tab = (e.target as any).parentElement as HTMLElement; - DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) => - (sourceDoc instanceof Doc) && DragManager.StartLinkTargetsDrag(tab, x, y, sourceDoc))); - } if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } @@ -508,24 +487,27 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } tabCreated = async (tab: any) => { + tab.titleElement[0].Tab = tab; if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { if (tab.contentItem.config.fixed) { tab.contentItem.parent.config.fixed = true; } const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; - const dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; if (doc instanceof Doc) { - const dragSpan = document.createElement("span"); - dragSpan.style.position = "relative"; - dragSpan.style.bottom = "6px"; - dragSpan.style.paddingLeft = "4px"; - dragSpan.style.paddingRight = "2px"; + //tab.titleElement[0].outerHTML = `<input class='lm_title' style="background:black" value='${doc.title}' />`; + tab.titleElement[0].onclick = (e: any) => tab.titleElement[0].focus(); + tab.titleElement[0].onchange = (e: any) => { + tab.titleElement[0].size = e.currentTarget.value.length + 1; + Doc.GetProto(doc).title = e.currentTarget.value, true; + }; + tab.titleElement[0].size = StrCast(doc.title).length + 1; + tab.titleElement[0].value = doc.title; const gearSpan = document.createElement("span"); + gearSpan.className = "collectionDockingView-gear"; gearSpan.style.position = "relative"; gearSpan.style.paddingLeft = "0px"; gearSpan.style.paddingRight = "12px"; - const upDiv = document.createElement("span"); const stack = tab.contentItem.parent; // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: any) => { @@ -541,24 +523,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp onPointerDown={e => { e.preventDefault(); e.stopPropagation(); - DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); - }}> - <FontAwesomeIcon icon="file" size="lg" /> - </span>, dragSpan); - ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan); - tab.reactComponents = [dragSpan, gearSpan, upDiv]; - tab.element.append(dragSpan); + const dragData = new DragManager.DocumentDragData([doc]); + dragData.dropAction = doc.dropAction === "alias" ? "alias" : doc.dropAction === "copy" ? "copy" : undefined; + DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); + }}><DockingViewButtonSelector Document={doc} Stack={stack} /></span>, gearSpan); + tab.reactComponents = [gearSpan]; tab.element.append(gearSpan); - tab.element.append(upDiv); - tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { - tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; - tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegreeUnmemoized(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegreeUnmemoized(doc)]} 1px`; + tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { + tab.titleElement[0].textContent = title, { fireImmediately: true }; + tab.titleElement[0].style.padding = degree ? 0 : 2; + tab.titleElement[0].style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; }); //TODO why can't this just be doc instead of the id? tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } } - tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler .click(async function () { tab.reactionDisposer && tab.reactionDisposer(); @@ -594,7 +573,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); + this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); } }); @@ -667,9 +646,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp interface DockedFrameProps { documentId: FieldId; - dataDocumentId: FieldId; glContainer: any; libraryPath: (FieldId[]); + backgroundColor?: (doc: Doc) => string | undefined; //collectionDockingView: CollectionDockingView } @observer @@ -679,7 +658,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; - @observable private _dataDoc: Opt<Doc>; @observable private _isActive: boolean = false; get _stack(): any { @@ -687,12 +665,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } constructor(props: any) { super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { - this._document = f as Doc; - if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) { - DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); - } - })); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); this.props.libraryPath && this.setupLibraryPath(); } @@ -708,24 +681,31 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { **/ @undoBatch @action - public PinDoc(doc: Doc) { + public static PinDoc(doc: Doc) { //add this new doc to props.Document const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; if (curPres) { - const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); - Doc.GetProto(pinDoc).presentationTargetDoc = doc; - Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title?.toString()'); - const data = Cast(curPres.data, listSpec(Doc)); - if (data) { - data.push(pinDoc); - } else { - curPres.data = new List([pinDoc]); - } + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + Doc.AddDocToList(curPres, "data", pinDoc); if (!DocumentManager.Instance.getDocumentView(curPres)) { - this.addDocTab(curPres, undefined, "onRight"); + CollectionDockingView.AddRightSplit(curPres); } } } + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static UnpinDoc(doc: Doc) { + //add this new doc to props.Document + const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; + if (curPres) { + const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); + ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); + } + } componentDidMount() { const observer = new _global.ResizeObserver(action((entries: any) => { @@ -757,8 +737,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; contentScaling = () => { if (this.layoutDoc!.type === DocumentType.PDF) { @@ -784,19 +764,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - get widthpercent() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; } + get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; } - addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string, libraryPath?: Doc[]) => { + addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); if (doc.dockingConfig) { return MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath); + return CollectionDockingView.AddRightSplit(doc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); } else { - return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc, libraryPath); + return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); } } @@ -804,7 +784,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { TraceMobx(); if (!this._document) return (null); const document = this._document; - const resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; + const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;// document.layout instanceof Doc ? document : this._dataDoc; return <DocumentView key={document[Id]} LibraryPath={this._libraryPath} Document={document} @@ -820,9 +800,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} addDocTab={this.addDocTab} - pinToPres={this.PinDoc} + pinToPres={DockedFrameRenderer.PinDoc} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} @@ -841,5 +821,5 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { </div >); } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); }); -Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); }); +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index e613bf411..9384eb381 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), - () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0), + () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), { fireImmediately: true } ); @@ -67,11 +67,11 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); } } - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + 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>) => () => { @@ -85,8 +85,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const flexDir: any = StrCast(this.Document.flexDirection); return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} - onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> + <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), flexDirection: flexDir }}> @@ -98,7 +98,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, - height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, }} > <DocumentView Document={pair.layout} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index e84b3b0dd..3c2cbb5b0 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Doc } from "../../../new_fields/Doc"; @@ -16,10 +16,12 @@ import { CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { anchorPoints, Flyout } from "../DocumentDecorations"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; library.add(faPalette); @@ -81,7 +83,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const key = StrCast(this.props.parent.props.Document.sectionFilter); const castedValue = this.getValue(this._heading); de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); - this.props.parent.drop(e, de); + this.props.parent.onInternalDrop(e, de); e.stopPropagation(); } }); @@ -258,7 +260,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @computed get contentLayout() { const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); - const style = this.props.parent; const collapsed = this._collapsed; + const style = this.props.parent; + const collapsed = this._collapsed; const chromeStatus = this.props.parent.props.Document._chromeStatus; const newEditableViewProps = { GetValue: () => "", diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx deleted file mode 100644 index 440b6856b..000000000 --- a/src/client/views/collections/CollectionPivotView.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { faEdit } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Set } from "typescript-collections"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, StrCast } from "../../../new_fields/Types"; -import { Docs } from "../../documents/Documents"; -import { EditableView } from "../EditableView"; -import { anchorPoints, Flyout } from "../TemplateMenu"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import "./CollectionPivotView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -import { CollectionTreeView } from "./CollectionTreeView"; -import React = require("react"); - -@observer -export class CollectionPivotView extends CollectionSubView(doc => doc) { - componentDidMount() { - this.props.Document._freeformLayoutEngine = "pivot"; - const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked; - if (!this.props.Document._facetCollection) { - const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true }); - facetCollection.target = this.props.Document; - this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]); - - const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)"; - const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); "; - facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name }); - this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name }); - this.props.Document._facetCollection = facetCollection; - this.props.Document._fitToBox = true; - } - } - bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth; - getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0); - - @computed get _allFacets() { - const facets = new Set<string>(); - this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - return facets.toArray(); - } - - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const facetCollection = this.props.Document._facetCollection; - if (facetCollection instanceof Doc) { - const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader); - if (found !== -1) { - (facetCollection.data as List<Doc>).splice(found, 1); - const docFilter = Cast(this.props.Document._docFilter, listSpec("string")); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); - } - } - } else { - const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true }); - const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; - const params = { layoutDoc: Doc.name, dataDoc: Doc.name, }; - newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables); - Doc.AddDocToList(facetCollection, "data", newFacet); - } - } - } - _canClick = false; - _facetWidthOnDown = 0; - @observable _facetWidth = 200; - onPointerDown = (e: React.PointerEvent) => { - this._canClick = true; - this._facetWidthOnDown = e.screenX; - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - e.preventDefault(); - } - - - @action - onPointerMove = (e: PointerEvent) => { - this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); - Math.abs(e.movementX) > 6 && (this._canClick = false); - } - @action - onPointerUp = (e: PointerEvent) => { - if (Math.abs(e.screenX - this._facetWidthOnDown) < 6 && this._canClick) { - this._facetWidth = this._facetWidth < 15 ? 200 : 0; - } - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - render() { - const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null); - const flyout = ( - <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}> - {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div> - ); - return !facetCollection ? (null) : - <div className="collectionPivotView" style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> - <div className={"pivotKeyEntry"}> - <EditableView - contents={this.props.Document.pivotField} - GetValue={() => StrCast(this.props.Document.pivotField)} - SetValue={value => { - if (value && value.length) { - this.props.Document.pivotField = value; - return true; - } - return false; - }} - /> - </div> - <div className="collectionPivotView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} > - <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} /> - </div> - <div className="collectionPivotView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}> - <div className="collectionPivotView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="collectionPivotView-button"> - <span className="collectionPivotView-span">Facet Filters</span> - <FontAwesomeIcon icon={faEdit} size={"lg"} /> - </div> - </Flyout> - </div> - <div className="collectionPivotView-tree" key="tree"> - <CollectionTreeView {...this.props} Document={facetCollection} /> - </div> - </div> - <div className="collectionPivotView-pivot" key="pivot" style={{ width: this.bodyPanelWidth() }}> - <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} /> - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 4eba5dc26..df7abad61 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -36,9 +36,10 @@ export interface CellProps { Document: Doc; fieldKey: string; renderDepth: number; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; + addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; - moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc, targetCollection: Doc | undefined, + addDocument: (document: Doc) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; setIsEditing: (isEditing: boolean) => void; @@ -246,7 +247,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { DocListCast(this.props.Document[this.props.fieldKey]). - forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run)); + forEach((doc, i) => value.startsWith(":=") ? + this.props.setComputed(value.substring(2), doc, this.props.rowProps.column.id!, i, this.props.col) : + this.applyToDoc(doc, i, this.props.col, script.run)); } }} /> diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 92dc8780e..507ee89e4 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -5,11 +5,13 @@ import "./CollectionSchemaView.scss"; import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons'; import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Flyout, anchorPoints } from "../DocumentDecorations"; import { ColumnType } from "./CollectionSchemaView"; import { faFile } from "@fortawesome/free-regular-svg-icons"; import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes); @@ -289,13 +291,11 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || - this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + if (keyOptions.length) { + this.onSelect(keyOptions[0]); + } else if (this._searchTerm !== "" && this.props.canAddNew) { + this.setSearchTerm(this._searchTerm || this._key); this.onSelect(this._searchTerm); - } else { - this.setSearchTerm(this._key); } } } @@ -336,7 +336,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; const options = keyOptions.map(key => { - return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + return <div key={key} className="key-option" onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; }); // if search term does not already exist as a group type, give option to create new group type @@ -354,7 +354,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { <div className="keys-dropdown"> <input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input> - <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}> + <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> {this.renderOptions()} </div> </div > diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 153bbd410..670d6dbb2 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -3,9 +3,9 @@ import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table"; import "./CollectionSchemaView.scss"; import { Transform } from "../../util/Transform"; import { Doc } from "../../../new_fields/Doc"; -import { DragManager, SetupDrag } from "../../util/DragManager"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; -import { Cast, FieldValue } from "../../../new_fields/Types"; +import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { ContextMenu } from "../ContextMenu"; import { action } from "mobx"; import { library } from '@fortawesome/fontawesome-svg-core'; @@ -135,6 +135,7 @@ export interface MovableRowProps { rowFocused: boolean; textWrapRow: (doc: Doc) => void; rowWrapped: boolean; + dropAction: string; } export class MovableRow extends React.Component<MovableRowProps> { @@ -219,7 +220,7 @@ export class MovableRow extends React.Component<MovableRowProps> { if (!doc) return <></>; const reference = React.createRef<HTMLDivElement>(); - const onItemDown = SetupDrag(reference, () => doc, this.move); + const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); let className = "collectionSchema-row"; if (this.props.rowFocused) className += " row-focused"; diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 8b3d332af..a24140b48 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -39,9 +39,9 @@ cursor: col-resize; } - .documentView-node:first-child { - background: $light-color; - } + // .documentView-node:first-child { + // background: $light-color; + // } } .ReactTable { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index fa8be5177..6eeceb552 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -14,7 +14,6 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ComputedField } from "../../../new_fields/ScriptField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { Gateway } from "../../northstar/manager/Gateway"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; @@ -29,6 +28,7 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -44,8 +44,8 @@ export enum ColumnType { // this map should be used for keys that should have a const type of value const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], - ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], - ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], + ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], + ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @@ -55,9 +55,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _startPreviewWidth = 0; private DIVIDER_WIDTH = 4; - @observable previewScript: string = ""; @observable previewDoc: Doc | undefined = undefined; - @observable private _node: HTMLDivElement | null = null; @observable private _focusedTable: Doc = this.props.Document; @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @@ -76,9 +74,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; - @undoBatch - @action setPreviewScript = (script: string) => this.previewScript = script - //toggles preview side-panel of schema @action toggleExpander = () => { @@ -87,27 +82,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onDividerDown = (e: React.PointerEvent) => { this._startPreviewWidth = this.previewWidth(); - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); + setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); } @action - onDividerMove = (e: PointerEvent): void => { + onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { const nativeWidth = this._mainCont!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; this.props.Document.schemaPreviewWidth = width; - } - @action - onDividerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); - if (this._startPreviewWidth === this.previewWidth()) { - this.toggleExpander(); - } + return false; } onPointerDown = (e: React.PointerEvent): void => { @@ -120,9 +105,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @computed - get previewDocument(): Doc | undefined { - return this.previewDoc ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(this.previewDoc[this.previewScript], Doc)) : this.previewDoc) : undefined; - } + get previewDocument(): Doc | undefined { return this.previewDoc; } getPreviewTransform = (): Transform => { return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); @@ -175,7 +158,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.props.ScreenToLocalTransform} active={this.props.active} - onDrop={this.onDrop} + onDrop={this.onExternalDrop} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} isSelected={this.props.isSelected} @@ -199,7 +182,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.onDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable} </div> {this.dividerDragger} @@ -225,7 +208,7 @@ export interface SchemaTableProps { ScreenToLocalTransform: () => Transform; active: (outsideReaction: boolean) => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; + addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc) => boolean; @@ -409,7 +392,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { rowInfo, rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document), textWrapRow: this.toggleTextWrapRow, - rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1 + rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1, + dropAction: StrCast(this.props.Document.childDropAction) }; } @@ -477,8 +461,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch createRow = () => { - const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }); - this.props.addDocument(newDoc); + this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); } @undoBatch @@ -559,16 +542,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { columns[index] = columnField; this.columns = columns; } - - // const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - // if (!typesDoc) { - // let newTypesDoc = new Doc(); - // newTypesDoc[key] = type; - // this.props.Document.schemaColumnTypes = newTypesDoc; - // return; - // } else { - // typesDoc[key] = type; - // } } @undoBatch @@ -692,7 +665,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }) + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); } } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 843c743db..bfa5ea278 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -19,6 +19,7 @@ position: absolute; top: 0; overflow-y: auto; + overflow-x: hidden; flex-wrap: wrap; transition: top .5s; >div { @@ -159,9 +160,7 @@ } .collectionStackingView-sectionHeader { text-align: center; - margin-left: 2px; - margin-right: 2px; - margin-top: 10px; + margin: auto; background: $main-accent; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong @@ -213,6 +212,7 @@ left: 0; top: 0; height: 100%; + display: none; [class*="css"] { max-width: 102px; @@ -250,6 +250,7 @@ right: 0; top: 0; height: 100%; + display: none; [class*="css"] { max-width: 102px; @@ -284,6 +285,18 @@ right: 25px; top: 0; height: 100%; + display: none; + } + } + .collectionStackingView-sectionHeader:hover { + .collectionStackingView-sectionColor { + display:unset; + } + .collectionStackingView-sectionOptions { + display:unset; + } + .collectionStackingView-sectionDelete { + display:unset; } } @@ -293,7 +306,6 @@ overflow: hidden; margin: auto; width: 90%; - color: lightgrey; overflow: ellipses; .editableView-container-editing-oneLine, diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 7592712e4..d1f45af90 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -9,22 +9,22 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types"; -import { emptyFunction, Utils } from "../../../Utils"; -import { DocumentType } from "../../documents/DocumentTypes"; +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 { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; -import { TraceMobx } from "../../../new_fields/util"; import { CollectionViewType } from "./CollectionView"; +import { Docs } from "../../documents/Documents"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -39,9 +39,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @observable _scroll = 0; // used to force the document decoration to update when scrolling @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } - @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); } - @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); } - @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); } + @computed get 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; } @@ -52,18 +52,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } - children(docs: Doc[]) { + children(docs: Doc[], columns?: number) { this._docXfs.length = 0; return docs.map((d, i) => { - const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); const height = () => this.getDocHeight(d); + const width = () => this.getDocWidth(d); const dref = React.createRef<HTMLDivElement>(); const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); - const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + 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, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)} + {this.getDisplayDoc(d, this.props.DataDoc, dxf, width)} </div>; }); } @@ -79,8 +79,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const sectionHeaders = this.sectionHeaders; + const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders); 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; // the next five lines ensures that floating point rounding errors don't create more than one section -syip @@ -96,8 +97,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); sectionHeaders.push(newSchemaHeader); + changed = true; } }); + changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0); return fields; } @@ -155,11 +158,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { - const layoutDoc = Doc.Layout(doc); + const layoutDoc = Doc.Layout(doc, this.props.childLayoutTemplate?.()); const height = () => this.getDocHeight(doc); return <ContentFittingDocumentView Document={doc} - DataDocument={dataDoc} + DataDocument={doc[DataSym] !== doc && doc[DataSym]} + backgroundColor={this.props.backgroundColor} + LayoutDoc={this.props.childLayoutTemplate} LibraryPath={this.props.LibraryPath} renderDepth={this.props.renderDepth + 1} fitToBox={this.props.fitToBox} @@ -179,42 +184,36 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { pinToPres={this.props.pinToPres}> </ContentFittingDocumentView>; } + + getDocWidth(d?: Doc) { + if (!d) return 0; + const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const nw = NumCast(layoutDoc._nativeWidth); + return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + } getDocHeight(d?: Doc) { if (!d) return 0; - const layoutDoc = Doc.Layout(d); + 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.ignoreAspect && !layoutDoc._fitWidth && nw && nh) { + if (!layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); runInAction(() => this._cursor = "grabbing"); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + setupMoveUpEvents(this, e, this.onDividerMove, action(() => this._cursor = "grab"), emptyFunction); } @action - onDividerMove = (e: PointerEvent): void => { - const dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - const delta = dragPos - this._columnStart; - this._columnStart = dragPos; - this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta); - } - - @action - onDividerUp = (e: PointerEvent): void => { - runInAction(() => this._cursor = "grab"); - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); + onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { + this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta[0]); + return false; } @computed get columnDragger() { @@ -226,8 +225,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @undoBatch @action - drop = (e: Event, de: DragManager.DropEvent) => { - console.log("DROP STACKIN G2"); + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const where = [de.x, de.y]; let targInd = -1; let plusOne = 0; @@ -241,7 +239,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); - if (super.drop(e, de)) { + if (super.onInternalDrop(e, de)) { const newDoc = de.complete.docDragData.droppedDocuments[0]; const docs = this.childDocList; if (docs) { @@ -257,8 +255,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @undoBatch @action - onDrop = async (e: React.DragEvent): Promise<void> => { - console.log("DROP STACKING"); + onExternalDrop = async (e: React.DragEvent): Promise<void> => { const where = [e.clientX, e.clientY]; let targInd = -1; this._docXfs.map((cd, i) => { @@ -268,7 +265,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { targInd = i; } }); - super.onDrop(e, {}, () => { + super.onExternalDrop(e, {}, () => { if (targInd !== -1) { const newDoc = this.childDocs[this.childDocs.length - 1]; const docs = this.childDocList; @@ -342,7 +339,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @action addGroup = (value: string) => { if (value && this.sectionHeaders) { - this.sectionHeaders.push(new SchemaHeaderField(value)); + const schemaHdrField = new SchemaHeaderField(value); + this.sectionHeaders.push(schemaHdrField); + Doc.addEnumerationToTextField(undefined, this.sectionFilter, [Docs.Create.TextDocument(value, { title: value, _backgroundColor: schemaHdrField.color })]); return true; } return false; @@ -364,8 +363,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); - subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); - subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); } } @@ -379,6 +376,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1])); } + + @computed get scaling() { return !this.props.Document._nativeWidth ? 1 : this.props.PanelHeight() / NumCast(this.props.Document._nativeHeight); } + render() { TraceMobx(); const editableViewProps = { @@ -392,12 +392,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ref={this.createRef} style={{ overflowY: this.props.active() ? "auto" : "hidden", - transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`, - height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`, - transformOrigin: "top" + transform: `scale(${this.scaling}`, + height: `${1 / this.scaling * 100}%`, + width: `${1 / this.scaling * 100}%`, + transformOrigin: "top left", }} onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} - onDrop={this.onDrop.bind(this)} + onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this.props.active() && e.stopPropagation()} > {this.renderedSections} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index c4680fc28..516e583d4 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -5,25 +5,28 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { RichTextField } from "../../../new_fields/RichTextField"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast } from "../../../new_fields/Types"; -import { Docs } from "../../documents/Documents"; +import { NumCast, StrCast, Cast } from "../../../new_fields/Types"; +import { ImageField } from "../../../new_fields/URLField"; +import { TraceMobx } from "../../../new_fields/util"; +import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { anchorPoints, Flyout } from "../DocumentDecorations"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; +import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; -import { TraceMobx } from "../../../new_fields/util"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageField } from "../../../new_fields/URLField"; -import { ImageBox } from "../nodes/ImageBox"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { RichTextField } from "../../../new_fields/RichTextField"; +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; library.add(faPalette); @@ -42,20 +45,15 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { @observable private _background = "inherit"; - @observable private _createAliasSelected: boolean = false; - private _dropRef: HTMLDivElement | null = null; private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; - private _sensitivity: number = 16; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; createColumnDropRef = (ele: HTMLDivElement | null) => { - this._dropRef = ele; - this.dropDisposer && this.dropDisposer(); + this.dropDisposer?.(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } @@ -63,18 +61,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { - console.log("column drop stacking"); - this._createAliasSelected = false; if (de.complete.docDragData) { const key = StrCast(this.props.parent.props.Document.sectionFilter); const castedValue = this.getValue(this._heading); - if (castedValue) { - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); - } - else { - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = undefined); - } - this.props.parent.drop(e, de); + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, false)); + this.props.parent.onInternalDrop(e, de); e.stopPropagation(); } }); @@ -94,7 +85,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action headingChanged = (value: string, shiftDown?: boolean) => { - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); const castedValue = this.getValue(value); if (castedValue) { @@ -115,7 +105,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action changeColumnColor = (color: string) => { - this._createAliasSelected = false; if (this.props.headingObject) { this.props.headingObject.setColor(color); this._color = color; @@ -125,22 +114,18 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => { if (SelectionManager.GetIsDragging()) { - this._createAliasSelected = false; this._background = "#b4b4b4"; } } @action pointerLeave = () => { - this._createAliasSelected = false; this._background = "inherit"; - document.removeEventListener("pointermove", this.startDrag); } @action addDocument = (value: string, shiftDown?: boolean) => { if (!value) return false; - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); @@ -152,7 +137,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action deleteColumn = () => { - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); this.props.docList.forEach(d => d[key] = undefined); if (this.props.parent.sectionHeaders && this.props.headingObject) { @@ -163,7 +147,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action collapseSection = () => { - this._createAliasSelected = false; if (this.props.headingObject) { this._headingsHack++; this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed); @@ -171,46 +154,23 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } } - startDrag = (e: PointerEvent) => { - 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); - let value = this.getValue(this._heading); - value = typeof value === "string" ? `"${value}"` : value; - alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); - if (alias.viewSpecScript) { - DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); - } - - e.stopPropagation(); - document.removeEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - } - } - - pointerUp = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - - document.removeEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - } - headerDown = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - - const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); - this._startDragPosition = { x: dx, y: dy }; + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + } - if (this._createAliasSelected) { - document.removeEventListener("pointermove", this.startDrag); - document.addEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - document.addEventListener("pointerup", this.pointerUp); + 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); + let value = this.getValue(this._heading); + value = typeof value === "string" ? `"${value}"` : value; + alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); + if (alias.viewSpecScript) { + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); + return true; } - runInAction(() => this._createAliasSelected = false); + return false; } renderColorPicker = () => { @@ -243,17 +203,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ); } - @action - toggleAlias = () => { - this._createAliasSelected = true; - } - renderMenu = () => { - const selected = this._createAliasSelected; return ( <div className="collectionStackingView-optionPicker"> <div className="optionOptions"> - <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> + <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div> </div> </div > ); @@ -269,8 +223,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ContextMenu.Instance.clearItems(); const layoutItems: ContextMenuProps[] = []; const docItems: ContextMenuProps[] = []; - const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; + + DocUtils.addDocumentCreatorMenuItems(this.props.parent.props.addDocument, this.props.parent.props.addDocument, x, y); + Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => docItems.push({ description: ":" + fieldKey, event: () => { @@ -288,8 +244,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC description: ":" + fieldKey, event: () => { const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); if (created) { - if (this.props.parent.Document.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + const container = this.props.parent.Document.resolvedDataDoc ? Doc.GetProto(this.props.parent.Document) : this.props.parent.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); } return this.props.parent.props.addDocument(created); } @@ -313,7 +271,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } }); const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(pt[0], pt[1]); + ContextMenu.Instance.displayMenu(x, y); } render() { @@ -325,6 +283,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { @@ -349,6 +308,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ + marginTop: NumCast(this.props.parent.props.Document._yMargin), width: (style.columnWidth) / ((uniqueHeadings.length + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) @@ -361,7 +321,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} style={{ width: "100%", - background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", + background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit", color: "grey" }}> <EditableView {...headerEditableViewProps} /> @@ -401,7 +361,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <div> <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} style={{ - padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, + padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, margin: "auto", width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', @@ -410,7 +370,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC gridTemplateColumns: singleColumn ? undefined : templatecols, gridAutoRows: singleColumn ? undefined : "0px" }}> - {this.props.parent.children(this.props.docList)} + {this.props.parent.children(this.props.docList, uniqueHeadings.length)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index c5028d16e..aa31d604e 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,7 +1,7 @@ -import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; -import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; +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"; @@ -25,6 +25,7 @@ import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; import { InteractionUtils } from "../../util/InteractionUtils"; +import { Upload } from "../../../server/SharedMediaTypes"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -33,7 +34,6 @@ export interface CollectionViewProps extends FieldViewProps { PanelWidth: () => number; PanelHeight: () => number; VisibleHeight?: () => number; - chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; fieldKey: string; } @@ -41,8 +41,11 @@ export interface CollectionViewProps extends FieldViewProps { 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) + 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) { @@ -53,12 +56,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { private _childLayoutDisposer?: IReactionDisposer; protected _mainCont?: HTMLDivElement; protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this.dropDisposer ?.(); - this.gestureDisposer ?.(); - this.multiTouchDisposer ?.(); + this.dropDisposer?.(); + this.gestureDisposer?.(); + this.multiTouchDisposer?.(); if (ele) { this._mainCont = ele; - this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + 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)); } @@ -68,39 +71,42 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc) ?.[Id]], - (args) => { - const childLayout = Cast(this.props.Document.childLayout, Doc); + this._childLayoutDisposer = reaction(() => ({ childDocs: this.childDocs, childLayout: Cast(this.props.Document.childLayout, Doc) }), + ({ childDocs, childLayout }) => { if (childLayout instanceof Doc) { - this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layout_fromParent")); + childDocs.map(doc => { + doc.layout_fromParent = childLayout; + doc.layoutKey = "layout_fromParent"; + }); } else if (!(childLayout instanceof Promise)) { - this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout")); + childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout")); } }, { fireImmediately: true }); } componentWillUnmount() { - this._childLayoutDisposer && this._childLayoutDisposer(); + this.gestureDisposer?.(); + this.multiTouchDisposer?.(); + this._childLayoutDisposer?.(); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } + @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() { - const { annotationsKey, fieldKey } = this.props; - if (annotationsKey) { - return this.dataDoc[fieldKey + "-" + annotationsKey]; - } - return this.dataDoc[fieldKey]; + return this.dataDoc[this.props.fieldKey + (this.props.annotationsKey ? "-" + this.props.annotationsKey : "")]; } 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); + 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 } get childDocList() { @@ -109,34 +115,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get childDocs() { const docs = DocListCast(this.dataField); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []); - const clusters: { [key: string]: { [value: string]: string } } = {}; - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); - const cluster = clusters[key]; - if (!cluster) { - const child: { [value: string]: string } = {}; - child[value] = modifiers; - clusters[key] = child; - } else { - cluster[value] = modifiers; - } - } - const filteredDocs = docFilters.length ? viewedDocs.filter(d => { - for (const key of Object.keys(clusters)) { - const cluster = clusters[key]; - const satisfiesFacet = Object.keys(cluster).some(inner => { - const modifier = cluster[inner]; - return (modifier === "x") !== Doc.matchFieldValue(d, key, inner); - }); - if (!satisfiesFacet) { - return false; - } - } - return true; - }) : viewedDocs; - return filteredDocs; + return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @action @@ -177,25 +156,22 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action - protected drop(e: Event, de: DragManager.DropEvent): boolean { + protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; (this.props.Document.dropConverter instanceof ScriptField) && this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this - if (docDragData && !docDragData.applyAsTemplate) { - if (de.altKey && docDragData.draggedDocuments.length) { - this.childDocs.map(doc => - Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layout_fromParent")); - e.stopPropagation(); - return true; - } + 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) { const movedDocs = docDragData.draggedDocuments; added = movedDocs.reduce((added: boolean, d, i) => docDragData.droppedDocuments[i] !== d ? this.props.addDocument(docDragData.droppedDocuments[i]) : - docDragData.moveDocument ?.(d, this.props.Document, this.props.addDocument) || added, false); + docDragData.moveDocument?.(d, this.props.Document, this.props.addDocument) || added, false); } else { added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -211,151 +187,172 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action - protected async onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { + protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; } - const html = e.dataTransfer.getData("text/html"); - const text = e.dataTransfer.getData("text/plain"); - console.log(html); + + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const text = dataTransfer.getData("text/plain"); if (text && text.startsWith("<div")) { return; } + e.stopPropagation(); e.preventDefault(); + const { addDocument } = this.props; + if (!addDocument) { + alert("this.props.addDocument does not exist. Aborting drop operation."); + return; + } - if (html && FormattedTextBox.IsFragment(html)) { - const href = FormattedTextBox.GetHref(html); - if (href) { - const docid = FormattedTextBox.GetDocFromUrl(href); - if (docid) { // prosemirror text containing link to dash document - DocServer.GetRefField(docid).then(f => { - if (f instanceof Doc) { - if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView - (f instanceof Doc) && this.props.addDocument(f); - } - }); + if (html) { + if (FormattedTextBox.IsFragment(html)) { + const href = FormattedTextBox.GetHref(html); + if (href) { + const docid = FormattedTextBox.GetDocFromUrl(href); + if (docid) { // prosemirror text containing link to dash document + DocServer.GetRefField(docid).then(f => { + if (f instanceof Doc) { + if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView + (f instanceof Doc) && addDocument(f); + } + }); + } else { + addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); + } + } else if (text) { + addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); + } + return; + } + 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] : ""; + if (img) { + const split = img.split("src=\"")[1].split("\"")[0]; + let source = split; + if (split.startsWith("data:image") && split.includes("base64")) { + 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); + return; } else { - this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); + const path = window.location.origin + "/doc/"; + if (text.startsWith(path)) { + const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0]; + DocServer.GetRefField(docid).then(f => { + if (f instanceof Doc) { + if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView + (f instanceof Doc) && this.props.addDocument(f); + } + }); + } else { + const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300 }); + Doc.GetProto(htmlDoc)["data-text"] = text; + this.props.addDocument(htmlDoc); + } + return; } - } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); } - return; } - if (html && !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] : ""; - if (img) { - const split = img.split("src=\"")[1].split("\"")[0]; - const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 }); - ImageUtils.ExtractExif(doc); - this.props.addDocument(doc); + + if (text) { + if (text.includes("www.youtube.com/watch")) { + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); + addDocument(Docs.Create.VideoDocument(url, { + ...options, + title: url, + _width: 400, + _height: 315, + _nativeWidth: 600, + _nativeHeight: 472.5 + })); return; - } else { - const path = window.location.origin + "/doc/"; - if (text.startsWith(path)) { - const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0]; - DocServer.GetRefField(docid).then(f => { - if (f instanceof Doc) { - if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView - (f instanceof Doc) && this.props.addDocument(f); - } - }); - } else { - const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text }); - this.props.addDocument(htmlDoc); - } + } + let matches: RegExpExecArray | null; + if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { + const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." }); + const proto = newBox.proto!; + const documentId = matches[2]; + proto[GoogleRef] = documentId; + proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; + proto.backgroundColor = "#eeeeff"; + addDocument(newBox); + return; + } + if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { + const albumId = matches[3]; + const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); + console.log(mediaItems); return; } } - if (text && text.indexOf("www.youtube.com/watch") !== -1) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 })); - return; - } - let matches: RegExpExecArray | null; - if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { - const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." }); - const proto = newBox.proto!; - const documentId = matches[2]; - proto[GoogleRef] = documentId; - proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; - proto.backgroundColor = "#eeeeff"; - this.props.addDocument(newBox); - // const parent = Docs.Create.StackingDocument([newBox], { title: `Google Doc Import (${documentId})` }); - // CollectionDockingView.Instance.AddRightSplit(parent, undefined); - // proto.height = parent[HeightSym](); + + const { items } = e.dataTransfer; + const { length } = items; + const files: File[] = []; + const generatedDocuments: Doc[] = []; + if (!length) { + alert("No uploadable content found."); return; } - if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { - const albums = await GooglePhotos.Transactions.ListAlbums(); - const albumId = matches[3]; - const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); - console.log(mediaItems); - } + const batch = UndoManager.StartBatch("collection view drop"); - const promises: Promise<void>[] = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < e.dataTransfer.items.length; i++) { + for (let i = 0; i < length; i++) { const item = e.dataTransfer.items[i]; - if (item.kind === "string" && item.type.indexOf("uri") !== -1) { - let str: string; - const prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(Utils.CorsProxy(str = s)))) - .then(result => { - const type = result["content-type"]; - if (type) { - Docs.Get.DocumentFromType(type, str, options) - .then(doc => doc && this.props.addDocument(doc)); - } - }); - promises.push(prom); + 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"]; + if (type) { + const doc = await Docs.Get.DocumentFromType(type, stringContents, options); + doc && generatedDocuments.push(doc); + } } - const type = item.type; if (item.kind === "file") { const file = item.getAsFile(); - const formData = new FormData(); - - if (!file || !file.type) { - continue; - } - - formData.append('file', file); - const dropFileName = file ? file.name : "-empty-"; - promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { - results.map(action((result: any) => { - const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result; - const full = { ...options, _width: 300, title: dropFileName }; - const pathname = Utils.prepend(clientAccessPath); - Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - if (doc) { - const proto = Doc.GetProto(doc); - proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); - nativeWidth && (proto["data-nativeWidth"] = nativeWidth); - nativeHeight && (proto["data-nativeHeight"] = nativeHeight); - contentSize && (proto.contentSize = contentSize); - this.props.addDocument(doc); - } - }); - })); - })); + file && file.type && files.push(file); } } - - if (promises.length) { - Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); + for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) { + if (result instanceof Error) { + alert(`Upload failed: ${result.message}`); + return; + } + const full = { ...options, _width: 300, title: name }; + const pathname = Utils.prepend(result.accessPaths.agnostic.client); + const doc = await Docs.Get.DocumentFromType(type, pathname, full); + if (!doc) { + continue; + } + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + if (Upload.isImageInformation(result)) { + proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400; + proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight); + proto.contentSize = result.contentSize; + } + generatedDocuments.push(doc); + } + if (generatedDocuments.length) { + generatedDocuments.forEach(addDocument); + completed && completed(); } else { if (text && !text.includes("https://")) { - this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); + addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); } - batch.end(); } + batch.end(); } } + return CollectionSubView; } diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionTimeView.scss index 505091e98..865fc3cd2 100644 --- a/src/client/views/collections/CollectionPivotView.scss +++ b/src/client/views/collections/CollectionTimeView.scss @@ -1,16 +1,55 @@ -.collectionPivotView { +.collectionTimeView, +.collectionTimeView-pivot { display: flex; flex-direction: row; position: absolute; height: 100%; width: 100%; + overflow: hidden; - .collectionPivotView-flyout { + .collectionTimeView-backBtn { + background: green; + display: inline; + } + + .collectionFreeform-customText { + text-align: left; + } + + .collectionFreeform-customDiv { + position: absolute; + } + + .collectionTimeView-thumb { + position: absolute; + width: 30px; + height: 30px; + transform: rotate(45deg); + display: inline-block; + background: gray; + bottom: 0; + margin-bottom: -17px; + border-radius: 9px; + opacity: 0.25; + } + + .collectionTimeView-thumb-min { + margin-left: 25%; + } + + .collectionTimeView-thumb-max { + margin-left: 75%; + } + + .collectionTimeView-thumb-mid { + margin-left: 50%; + } + + .collectionTimeView-flyout { width: 400px; - height: 300px; display: inline-block; - .collectionPivotView-flyout-item { + .collectionTimeView-flyout-item { background-color: lightgray; text-align: left; display: inline-block; @@ -27,28 +66,32 @@ pointer-events: all; padding: 5px; border: 1px solid black; + display:none; + span { + margin-left : 10px; + } } - .collectionPivotView-treeView { + .collectionTimeView-treeView { display: flex; flex-direction: column; width: 200px; height: 100%; - .collectionPivotView-addfacet { + .collectionTimeView-addfacet { display: inline-block; width: 200px; height: 30px; background: darkGray; text-align: center; - .collectionPivotView-button { + .collectionTimeView-button { align-items: center; display: flex; width: 100%; height: 100%; - .collectionPivotView-span { + .collectionTimeView-span { margin: auto; } } @@ -61,20 +104,20 @@ } } - .collectionPivotView-tree { + .collectionTimeView-tree { display: inline-block; width: 100%; height: calc(100% - 30px); } } - .collectionPivotView-pivot { + .collectionTimeView-innards { display: inline-block; width: calc(100% - 200px); height: 100%; } - .collectionPivotView-dragger { + .collectionTimeView-dragger { background-color: lightgray; height: 40px; width: 20px; @@ -85,4 +128,16 @@ z-index: 2; left: -10px; } +} + +.collectionTimeView-pivot { + .collectionFreeform-customText { + text-align: center; + } +} + +.collectionTimeView:hover, .collectionTimeView-pivot:hover { + .pivotKeyEntry { + display:unset; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx new file mode 100644 index 000000000..50e297f0b --- /dev/null +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -0,0 +1,378 @@ +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction, trace } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, Field, WidthSym, HeightSym } 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 { Scripting } from "../../util/Scripting"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { EditableView } from "../EditableView"; +import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines"; +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; +import React = require("react"); + +@observer +export class CollectionTimeView extends CollectionSubView(doc => doc) { + _changing = false; + @observable _layoutEngine = "pivot"; + + 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; + } + 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); + + _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); + } + + 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); + } + + 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); + } + + 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> + </div>; + } + + public static SyncTimelineToPresentation(doc: Doc) { + const fieldKey = Doc.LayoutFieldKey(doc); + doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(curPresentationItem()[this._pivotField || 'year'] || 0)"); + } + specificMenu = (e: React.MouseEvent) => { + const layoutItems: ContextMenuProps[] = []; + const doc = this.props.Document; + + layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" }); + + ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" }); + } + + render() { + const newEditableViewProps = { + GetValue: () => "", + SetValue: (value: any) => { + if (value?.length) { + this.props.Document._pivotField = value; + return true; + } + return false; + }, + showMenuOnLoad: true, + contents: ":" + StrCast(this.props.Document._pivotField), + toggle: this.toggleVisibility, + color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + }; + + let nonNumbers = 0; + this.childDocs.map(doc => { + const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)]))); + if (Number.isNaN(num)) { + nonNumbers++; + } + }); + const forceLayout = StrCast(this.props.Document._forceRenderEngine); + const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; + if (doTimeline !== (this._layoutEngine === "timeline")) { + if (!this._changing) { + this._changing = true; + setTimeout(action(() => { + this._layoutEngine = doTimeline ? "timeline" : "pivot"; + this._changing = false; + }), 0); + } + } + + + 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>; + } +} + +Scripting.addGlobal(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { + let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); + pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); + pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + pivotDoc._prevFilterIndex = ++prevFilterIndex; + runInAction(() => { + pivotDoc._docFilters = new List(); + (bounds.payload as string[]).map(filterVal => + Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check")); + }); +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 2fa6813d7..6ebe81545 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -63,7 +63,9 @@ font-size: 8pt; margin-left: 3px; display: none; - background: lightgray; +} +.collectionTreeView-keyHeader:hover { + background: #797777; } .collectionTreeView-subtitle { @@ -84,9 +86,11 @@ .treeViewItem-openRight { display: none; height: 17px; - background: gray; width: 15px; } +.treeViewItem-openRight:hover { + background: #797777; +} .treeViewItem-border { display: inherit; @@ -101,7 +105,6 @@ .treeViewItem-openRight { display: inline-block; height: 17px; - background: #a8a7a7; width: 15px; // display: inline; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a7733ab5f..7eeeb6ff1 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -3,7 +3,7 @@ import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc'; +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'; @@ -34,7 +34,6 @@ import "./CollectionTreeView.scss"; import React = require("react"); import { CollectionViewType } from './CollectionView'; import { RichTextField } from '../../../new_fields/RichTextField'; -import { ObjectField } from '../../../new_fields/ObjectField'; export interface TreeViewProps { @@ -47,7 +46,7 @@ export interface TreeViewProps { deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; + addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; @@ -56,14 +55,16 @@ export interface TreeViewProps { indentDocument?: () => void; outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; + backgroundColor?: (doc: Doc) => string | undefined; outerXf: () => { translateX: number, translateY: number }; treeViewId: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; - hideHeaderFields: () => boolean; - preventTreeViewOpen: boolean; + treeViewHideHeaderFields: () => boolean; + treeViewPreventOpen: boolean; renderedIds: string[]; onCheckedClick?: ScriptField; + ignoreFields?: string[]; } library.add(faTrashAlt); @@ -84,11 +85,10 @@ library.add(faPlus, faMinus); * * special fields: * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden - * preventTreeViewOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - static loadId = ""; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); @@ -97,8 +97,8 @@ class TreeView extends React.Component<TreeViewProps> { get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get treeViewOpen() { return (!this.props.preventTreeViewOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } + 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 dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @@ -128,7 +128,7 @@ class TreeView extends React.Component<TreeViewProps> { } @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath); + @undoBatch openRight = () => this.props.addDocTab(this.props.containingCollection.childDropAction === "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,7 +171,7 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline-block"} - editing={this.dataDoc[Id] === TreeView.loadId} + editing={this.dataDoc[Id] === EditableView.loadId} contents={StrCast(this.props.document[key])} height={12} fontStyle={style} @@ -180,30 +180,30 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - TreeView.loadId = doc[Id]; + 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]; return this.props.addDocument(doc); })} OnTab={undoBatch((shift?: boolean) => { - TreeView.loadId = this.dataDoc[Id]; + EditableView.loadId = this.dataDoc[Id]; shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); setTimeout(() => { // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document). Doc.UnBrushDoc(this.props.document); Doc.BrushDoc(this.props.document); - TreeView.loadId = ""; + EditableView.loadId = ""; }, 0); })} />) onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view + const sort = this.props.document[`${this.fieldKey}-sortAscending`]; if (this.props.document === CurrentUserUtils.UserDocument.recentlyClosed) { ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.GetProto(CurrentUserUtils.UserDocument.recentlyClosed as Doc).data = new List<Doc>(), icon: "plus" }); } else if (this.props.document !== CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab", this.props.libraryPath), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab", this.props.libraryPath), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath), icon: "caret-square-right" }); if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.props.document)), icon: "camera" }); } @@ -212,7 +212,9 @@ class TreeView extends React.Component<TreeViewProps> { ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); } - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: (sort ? "Sort Descending" : (sort === false ? "Unsort" : "Sort Ascending")), event: () => this.props.document[`${this.fieldKey}-sortAscending`] = (sort ? false : (sort === false ? undefined : true)), icon: "minus" }); + ContextMenu.Instance.addItem({ description: "Toggle Theme Colors", event: () => this.props.document.darkScheme = !this.props.document.darkScheme, icon: "minus" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); @@ -229,7 +231,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 }); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree drop link"); e.stopPropagation(); } if (de.complete.docDragData) { @@ -283,6 +285,7 @@ class TreeView extends React.Component<TreeViewProps> { const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { + if (this.props.ignoreFields?.includes(key)) continue; const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; @@ -291,13 +294,13 @@ class TreeView extends React.Component<TreeViewProps> { const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, - this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick); + 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); } else { contentElement = <EditableView key="editableView" - contents={contents !== undefined ? contents.toString() : "null"} + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} height={13} fontSize={12} GetValue={() => Field.toKeyValueString(doc, key)} @@ -334,9 +337,9 @@ class TreeView extends React.Component<TreeViewProps> { {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)} + 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)} </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}> @@ -350,6 +353,7 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} + backgroundColor={this.props.backgroundColor} fitToBox={this.boundsOfCollectionDocument !== undefined} PanelWidth={this.docWidth} PanelHeight={this.docHeight} @@ -386,7 +390,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" : "black"), 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: 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>; } @@ -417,7 +421,6 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ - color: this.props.document.isMinimized ? "red" : "black", 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, @@ -425,7 +428,7 @@ class TreeView extends React.Component<TreeViewProps> { }} > {this.editableView("title")} </div > - {this.props.hideHeaderFields() ? (null) : headerElements} + {this.props.treeViewHideHeaderFields() ? (null) : headerElements} {openRight} </>; } @@ -456,19 +459,21 @@ class TreeView extends React.Component<TreeViewProps> { remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean, + addDocTab: (doc: Doc, where: string) => boolean, pinToPres: (document: Doc) => void, + backgroundColor: undefined | ((document: Doc) => string | undefined), screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: (outsideReaction?: boolean) => boolean, panelWidth: () => number, ChromeHeight: undefined | (() => number), renderDepth: number, - hideHeaderFields: () => boolean, - preventTreeViewOpen: boolean, + treeViewHideHeaderFields: () => boolean, + treeViewPreventOpen: boolean, renderedIds: string[], libraryPath: Doc[] | undefined, - onCheckedClick: ScriptField | undefined + onCheckedClick: ScriptField | undefined, + ignoreFields: string[] | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -476,10 +481,8 @@ class TreeView extends React.Component<TreeViewProps> { } const docs = childDocs.slice(); - const dataExtension = containingCollection[key + "_ext"] as Doc; - const ascending = dataExtension && BoolCast(dataExtension.sortAscending, null); + const ascending = containingCollection?.[key + "-sortAscending"]; if (ascending !== undefined) { - const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { const reN = /[0-9]*$/; const aA = a.replace(reN, ""); // get rid of trailing numbers @@ -563,6 +566,7 @@ class TreeView extends React.Component<TreeViewProps> { renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} + backgroundColor={backgroundColor} panelWidth={rowWidth} panelHeight={rowHeight} ChromeHeight={ChromeHeight} @@ -574,9 +578,10 @@ class TreeView extends React.Component<TreeViewProps> { outerXf={outerXf} parentKey={key} active={active} - hideHeaderFields={hideHeaderFields} - preventTreeViewOpen={preventTreeViewOpen} - renderedIds={renderedIds} />; + treeViewHideHeaderFields={treeViewHideHeaderFields} + treeViewPreventOpen={treeViewPreventOpen} + renderedIds={renderedIds} + ignoreFields={ignoreFields} />; }); } } @@ -591,7 +596,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer && this.treedropDisposer(); if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); } } @@ -602,13 +607,24 @@ export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document): boolean => { - const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const children = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []); if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); return true; } return false; } + @action + addDoc = (doc: Document, relativeTo: Opt<Doc>, before?: boolean): boolean => { + 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()); + } else { + doAddDoc(); + } + return true; + } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) { @@ -624,44 +640,59 @@ export class CollectionTreeView extends CollectionSubView(Document) { ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } else { const layoutItems: ContextMenuProps[] = []; - layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" }); - layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" }); + layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" }); + layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" }); + layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); } ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { DocListCast(d.data).map((img, i) => { - const caption = (d.captions as any)[i]?.data; - if (caption instanceof ObjectField) { - Doc.GetProto(img).caption = ObjectField.MakeCopy(caption as ObjectField); + const caption = (d.captions as any)[i]; + if (caption) { + Doc.GetProto(img).caption = caption; } - img._hideSidebar = true; - d.captions = undefined; }); }); - const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create; + const { TextDocument, ImageDocument, CarouselDocument, TreeDocument } = Docs.Create; const { Document } = this.props; const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; - const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`; + const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`; const textDoc = TextDocument("", { title: "details", _autoHeight: true }); - const detailedLayout = Docs.Create.StackingDocument([ + const detailView = Docs.Create.StackingDocument([ CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }), textDoc, - ], { _chromeStatus: "disabled", title: "detailed layout stack" }); - textDoc.data = new RichTextField(detailedTemplate, "short_description year company"); - detailedLayout.isTemplateDoc = makeTemplate(detailedLayout); - - const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? - cardLayout.proto!.layout = ImageBox.LayoutString("hero"); - cardLayout.showTitle = "title"; - cardLayout.showTitleHover = "titlehover"; - - Document.childLayout = cardLayout; - Document.childDetailed = detailedLayout; - Document._viewType = CollectionViewType.Pivot; - Document.pivotField = "company"; + TextDocument("", { title: "shortDescription", _autoHeight: true }), + TreeDocument([], { title: "narratives", _height: 75, treeViewHideTitle: true }) + ], { _chromeStatus: "disabled", _width: 300, _height: 300, _autoHeight: true, title: "detailView" }); + textDoc.data = new RichTextField(detailedTemplate, "year company"); + detailView.isTemplateDoc = makeTemplate(detailView); + + const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? + heroView.proto!.layout = ImageBox.LayoutString("hero"); + heroView._showTitle = "title"; + heroView._showTitleHover = "titlehover"; + + Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", + Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), title: "hero view", icon: "portrait" + })); + + Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", + Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "file-alt" + })); + + Document.childLayout = heroView; + Document.childDetailed = detailView; + Document._viewType = CollectionViewType.Time; + Document._forceActive = true; + Document._pivotField = "company"; + Document.childDropAction = "alias"; } }); const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); @@ -673,7 +704,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); - onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); + onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); @computed get renderClearButton() { return <div id="toolbar" key="toolbar"> @@ -685,18 +716,20 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - const dropAction = StrCast(this.props.Document._dropAction) as dropActionType; - const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); + const dropAction = StrCast(this.props.Document.dropAction) 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); - return !this.childDocs ? (null) : ( + const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; + return !childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" - style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} + style={{ background: this.props.backgroundColor?.(this.props.Document), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> {(this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} + editing={false} display={"block"} maxHeight={72} height={"auto"} @@ -704,18 +737,17 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.dataDoc, "title", value, false); - const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - TreeView.loadId = doc[Id]; - Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); + 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.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { - TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields), - BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick)) + 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) } </ul> </div > @@ -728,7 +760,14 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey const facetValues = Array.from(allCollectionDocs.reduce((set, child) => set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); - const facetValueDocSet = facetValues.sort().map(facetValue => + let nonNumbers = 0; + facetValues.map(val => { + const num = Number(val); + if (Number.isNaN(num)) { + nonNumbers++; + } + }); + const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => Docs.Create.TextDocument("", { title: facetValue.toString(), treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", @@ -739,7 +778,7 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey }); Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { - const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []); + const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); for (let i = 0; i < docFilters.length; i += 3) { const [header, value, state] = docFilters.slice(i, i + 3); if (header === facetHeader && value === facetValue) { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index ab6ac0eaf..2d56f00d5 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { 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'; @@ -29,13 +29,16 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionLinearView } from './CollectionLinearView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; -import { CollectionPivotView } from './CollectionPivotView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionStaffView } from './CollectionStaffView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { CollectionTimeView } from './CollectionTimeView'; +import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; +import { List } from '../../../new_fields/List'; +import { SubCollectionViewProps } from './CollectionSubView'; 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); @@ -49,11 +52,11 @@ export enum CollectionViewType { Stacking, Masonry, Multicolumn, - Pivot, + Multirow, + Time, Carousel, Linear, - Staff, - Timeline + Staff } export namespace CollectionViewType { @@ -66,7 +69,8 @@ export namespace CollectionViewType { ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], ["multicolumn", CollectionViewType.Multicolumn], - ["pivot", CollectionViewType.Pivot], + ["multirow", CollectionViewType.Multirow], + ["time", CollectionViewType.Time], ["carousel", CollectionViewType.Carousel], ["linear", CollectionViewType.Linear], ]); @@ -86,11 +90,9 @@ export interface CollectionRenderProps { export class CollectionView extends Touchable<FieldViewProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } - private _reactionDisposer: IReactionDisposer | undefined; private _isChildActive = false; //TODO should this be observable? @observable private _isLightboxOpen = false; @observable private _curLightboxImg = 0; - @observable private _collapsed = true; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } @@ -107,31 +109,15 @@ export class CollectionView extends Touchable<FieldViewProps> { return viewField; } - componentDidMount = () => { - this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus), - () => { - // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar. - const chromeStatus = this.props.Document._chromeStatus; - if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { - runInAction(() => this._collapsed = true); - } - }); - } - - componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer(); - - // bcz: Argh? What's the height of the collection chromes?? - chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0); - active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; @action.bound addDocument(doc: Doc): boolean { - const targetDataDoc = Doc.GetProto(this.props.Document); - Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + 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 + // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); Doc.GetProto(doc).lastOpened = new DateField; return true; @@ -139,15 +125,17 @@ export class CollectionView extends Touchable<FieldViewProps> { @action.bound removeDocument(doc: Doc): boolean { + const targetDataDoc = this.props.Document[DataSym]; const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); - const value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const value = DocListCast(targetDataDoc[this.props.fieldKey]); 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 && ContextMenu.Instance.clearItems(); if (index !== -1) { value.splice(index, 1); + targetDataDoc[this.props.fieldKey] = new List<Doc>(value); return true; } return false; @@ -173,18 +161,19 @@ export class CollectionView extends Touchable<FieldViewProps> { } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - const props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" }; + const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); - case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); - case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + case CollectionViewType.Staff: return (<CollectionStaffView key="collview" {...props} />); + case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView key="collview" {...props} />); + case CollectionViewType.Multirow: return (<CollectionMultirowView key="rpwview" {...props} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); } case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } - case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); } + case CollectionViewType.Time: { return (<CollectionTimeView key="collview" {...props} />); } case CollectionViewType.Freeform: default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } @@ -192,13 +181,12 @@ export class CollectionView extends Touchable<FieldViewProps> { @action private collapse = (value: boolean) => { - this._collapsed = value; this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : + const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) : <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; return [chrome, this.SubViewHelper(type, renderProps)]; } @@ -223,9 +211,10 @@ export class CollectionView extends Touchable<FieldViewProps> { }); subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" }); subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" }); + subItems.push({ description: "Multirow", event: () => this.props.Document._viewType = CollectionViewType.Multirow, icon: "columns" }); subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" }); subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" }); - subItems.push({ description: "Pivot", event: () => this.props.Document._viewType = CollectionViewType.Pivot, icon: "columns" }); + subItems.push({ description: "Pivot/Time", event: () => this.props.Document._viewType = CollectionViewType.Time, icon: "columns" }); switch (this.props.Document._viewType) { case CollectionViewType.Freeform: { subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); @@ -239,22 +228,27 @@ export class CollectionView extends Touchable<FieldViewProps> { const layoutItems = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); if (this.props.Document.childLayout instanceof Doc) { - layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" }); + layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); } if (this.props.Document.childDetailed instanceof Doc) { - layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" }); + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, "onRight"), icon: "project-diagram" }); } !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); - const more = ContextMenu.Instance.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); - !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + const open = ContextMenu.Instance.findByDescription("Open..."); + const openItems = open && "subitems" in open ? open.subitems : []; + !open && ContextMenu.Instance.addItem({ description: "Open...", subitems: openItems, icon: "hand-point-right" }); const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + + 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" }); + } } @@ -286,7 +280,8 @@ export class CollectionView extends Touchable<FieldViewProps> { return (<div className={"collectionView"} style={{ pointerEvents: this.props.Document.isBackground ? "none" : "all", - boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` + boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : + `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31)" : "#9c9396"} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }} onContextMenu={this.onContextMenu}> {this.showIsTagged()} diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 414bbfc0b..8602b2369 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -2,7 +2,8 @@ @import '~js-datepicker/dist/datepicker.min.css'; .collectionViewChrome-cont { - position: relative; + position: absolute; + width:100%; opacity: 0.9; z-index: 9001; transition: top .5s; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 0811654bf..4f504ab1c 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -70,7 +70,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; - case CollectionViewType.Pivot: return this._freeform_commands; + case CollectionViewType.Time: return this._freeform_commands; case CollectionViewType.Carousel: return this._freeform_commands; } return []; @@ -179,7 +179,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); - }; + } @action openDatePicker = (e: React.PointerEvent) => { @@ -257,6 +257,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } subChrome = () => { + 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} />); @@ -270,32 +272,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro return this.props.CollectionView.props.Document; } - private get pivotKey() { - return StrCast(this.document.pivotField); - } - - private set pivotKey(value: string) { - this.document.pivotField = value; - } - - @observable private pivotKeyDisplay = this.pivotKey; - getPivotInput = () => { - if (StrCast(this.document._freeformLayoutEngine) !== "pivot") { - return (null); - } - return (<input className="collectionViewBaseChrome-viewSpecsInput" - placeholder="PIVOT ON..." - value={this.pivotKeyDisplay} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => this.pivotKeyDisplay = e.currentTarget.value)} - onKeyPress={action((e: React.KeyboardEvent<HTMLInputElement>) => { - const value = e.currentTarget.value; - if (e.which === 13) { - this.pivotKey = value; - this.pivotKeyDisplay = ""; - } - })} />); - } - @action.bound clearFilter = () => { this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name }); @@ -394,7 +370,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro 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"> + <div className="collectionViewChrome" style={{ border: "unset" }}> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" style={{ @@ -410,16 +386,17 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro 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="2">Schema</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Multicolumn</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Pivot</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Carousel</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Linear</option> + <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-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} > @@ -464,7 +441,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro </div> </div> </div> - <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > + <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> diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index a266861bd..4e704b58f 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -35,6 +35,10 @@ pointer-events: all; position: relative; display: inline-block; + svg { + width:20px !important; + height:20px; + } } .parentDocumentSelector-metadata { pointer-events: auto; @@ -46,8 +50,7 @@ div { overflow: visible !important; } - position: absolute; display: inline-block; - padding-left: 5px; - padding-right: 5px; + width:100%; + height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 115f8d633..43ba5c614 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -11,21 +11,20 @@ import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faEdit, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; +import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; -import { MetadataEntryMenu } from "../MetadataEntryMenu"; import { DocumentView } from "../nodes/DocumentView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faEdit); +library.add(faCog); type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, - addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void + addDocTab(doc: Doc, location: string): void }; @observer @@ -61,7 +60,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col._panX = newPanX; col._panY = newPanY; } - this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? + this.props.addDocTab(col, "inTab"); // bcz: dataDoc? }; } @@ -79,13 +78,12 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { export class ParentDocSelector extends React.Component<SelectorProps> { render() { const flyout = ( - <div className="parentDocumentSelector-flyout" style={{}} title=" "> + <div className="parentDocumentSelector-flyout" title=" "> <SelectorContextMenu {...this.props} /> </div> ); - return <div title="Tap to View Contexts/Metadata" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={flyout}> + return <div title="Show Contexts" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> <span className="parentDocumentSelector-button" > <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> </span> @@ -95,14 +93,9 @@ export class ParentDocSelector extends React.Component<SelectorProps> { } @observer -export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { +export class DockingViewButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { @observable hover = false; - @action - onPointerDown = (e: React.PointerEvent) => { - this.hover = !this.hover; - e.stopPropagation(); - } customStylesheet(styles: any) { return { ...styles, @@ -120,9 +113,9 @@ export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any <DocumentButtonBar views={[view]} stack={this.props.Stack} /> </div> ); - return <span title="Tap for menu" onPointerDown={e => e.stopPropagation()} className="buttonSelector"> + 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}> - <FontAwesomeIcon icon={faEdit} size={"sm"} /> + <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 be1317b25..637c81fe2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,82 +1,170 @@ import { Doc, Field, FieldResult } from "../../../../new_fields/Doc"; -import { NumCast, StrCast, Cast, DateCast } from "../../../../new_fields/Types"; +import { NumCast, StrCast, Cast } from "../../../../new_fields/Types"; import { ScriptBox } from "../../ScriptBox"; import { CompileScript } from "../../../util/Scripting"; import { ScriptField } from "../../../../new_fields/ScriptField"; import { OverlayView, OverlayElementOptions } from "../../OverlayView"; -import { emptyFunction } from "../../../../Utils"; +import { emptyFunction, aggregateBounds } from "../../../../Utils"; import React = require("react"); -import { ObservableMap, runInAction } from "mobx"; import { Id, ToString } from "../../../../new_fields/FieldSymbols"; import { ObjectField } from "../../../../new_fields/ObjectField"; import { RefField } from "../../../../new_fields/RefField"; -interface PivotData { +export interface ViewDefBounds { type: string; - text: string; + text?: string; x: number; y: number; - width: number; - height: number; - fontSize: number; + z?: number; + zIndex?: number; + width?: number; + height?: number; + transition?: string; + fontSize?: number; + highlight?: boolean; + color?: string; + payload: any; } -export interface ViewDefBounds { - x: number; - y: number; +export interface PoolData { + x?: number; + y?: number; z?: number; - width: number; - height: number; + zIndex?: number; + width?: number; + height?: number; + color?: string; transition?: string; + highlight?: boolean; } export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; } - function toLabel(target: FieldResult<Field>) { + if (typeof target === "number" || Number(target)) { + const truncated = Number(Number(target).toFixed(0)); + const precise = Number(Number(target).toFixed(2)); + return truncated === precise ? Number(target).toFixed(0) : Number(target).toFixed(2); + } if (target instanceof ObjectField || target instanceof RefField) { return target[ToString](); } return String(target); } +/** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ +function getTextWidth(text: string, font: string): number { + // re-use canvas object for better performance + const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas")); + const context = canvas.getContext("2d"); + context.font = font; + const metrics = context.measureText(text); + return metrics.width; +} + +interface PivotColumn { + docs: Doc[]; + filters: string[]; +} + -export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) { - const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); - const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); +export function computePivotLayout( + poolData: Map<string, PoolData>, + pivotDoc: Doc, + childDocs: Doc[], + filterDocs: Doc[], + childPairs: { layout: Doc, data?: Doc }[], + panelDim: number[], + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] +) { + const fieldKey = "data"; + const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>(); - const pivotFieldKey = toLabel(pivotDoc.pivotField); - for (const doc of childDocs) { + const pivotFieldKey = toLabel(pivotDoc._pivotField); + for (const doc of filterDocs) { const val = Field.toString(doc[pivotFieldKey] as Field); if (val) { - !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []); - pivotColumnGroups.get(val)!.push(doc); + !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val] }); + pivotColumnGroups.get(val)!.docs.push(doc); + } + } + let nonNumbers = 0; + childDocs.map(doc => { + const num = toNumber(doc[pivotFieldKey]); + if (num === undefined || Number.isNaN(num)) { + nonNumbers++; + } + }); + const pivotNumbers = nonNumbers / childDocs.length < .1; + if (pivotColumnGroups.size > 10) { + const arrayofKeys = Array.from(pivotColumnGroups.keys()); + const sortedKeys = pivotNumbers ? arrayofKeys.sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : arrayofKeys.sort(); + const clusterSize = Math.ceil(pivotColumnGroups.size / 10); + const numClusters = Math.ceil(sortedKeys.length / clusterSize); + for (let i = 0; i < numClusters; i++) { + for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) { + const curgrp = pivotColumnGroups.get(sortedKeys[i * clusterSize])!; + const newgrp = pivotColumnGroups.get(sortedKeys[j])!; + curgrp.docs.push(...newgrp.docs); + curgrp.filters.push(...newgrp.filters); + pivotColumnGroups.delete(sortedKeys[j]); + } + } + } + const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); + const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`; + const textlen = Array.from(pivotColumnGroups.keys()).map(c => getTextWidth(toLabel(c), desc)).reduce((p, c) => Math.max(p, c), 0 as number); + const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2); + const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1); + + const colWidth = panelDim[0] / pivotColumnGroups.size; + const colHeight = panelDim[1] - max_text; + let numCols = 0; + let bestArea = 0; + let pivotAxisWidth = 0; + for (let i = 1; i < 10; i++) { + const numInCol = Math.ceil(maxInColumn / i); + const hd = colHeight / numInCol; + const wd = colWidth / i; + const dim = Math.min(hd, wd); + if (dim > bestArea) { + bestArea = dim; + numCols = i; + pivotAxisWidth = dim; } } - const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity); - let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); const docMap = new Map<Doc, ViewDefBounds>(); - const groupNames: PivotData[] = []; - numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols); + const groupNames: ViewDefBounds[] = []; const expander = 1.05; const gap = .15; let x = 0; - pivotColumnGroups.forEach((val, key) => { + const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort(); + sortedPivotKeys.forEach(key => { + const val = pivotColumnGroups.get(key)!; let y = 0; let xCount = 0; + const text = toLabel(key); groupNames.push({ type: "text", - text: toLabel(key), + text, x, - y: pivotAxisWidth + 50, + y: pivotAxisWidth, width: pivotAxisWidth * expander * numCols, - height: NumCast(pivotDoc.pivotFontSize, 10), - fontSize: NumCast(pivotDoc.pivotFontSize, 10) + height: max_text, + fontSize, + payload: val }); - for (const doc of val) { + for (const doc of val.docs) { const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; @@ -85,10 +173,12 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; } docMap.set(doc, { - x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0), - y: -y, + type: "doc", + x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0), + y: -y + (pivotAxisWidth - hgt) / 2, width: wid, - height: hgt + height: hgt, + payload: undefined }); xCount++; if (xCount >= numCols) { @@ -99,21 +189,169 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo x += pivotAxisWidth * (numCols * expander + gap); }); - childPairs.map(pair => { - const defaultPosition = { - x: NumCast(pair.layout.x), - y: NumCast(pair.layout.y), - z: NumCast(pair.layout.z), - width: NumCast(pair.layout._width), - height: NumCast(pair.layout._height) - }; - const pos = docMap.get(pair.layout) || defaultPosition; - const data = poolData.get(pair.layout[Id]); - if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height) { - runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos })); + 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); + return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, [], childDocs.filter(c => !filterDocs.includes(c))); +} + +function toNumber(val: FieldResult<Field>) { + return val === undefined ? undefined : NumCast(val, Number(StrCast(val))); +} + +export function computeTimelineLayout( + poolData: Map<string, PoolData>, + pivotDoc: Doc, + childDocs: Doc[], + filterDocs: Doc[], + childPairs: { layout: Doc, data?: Doc }[], + panelDim: number[], + viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] +) { + const fieldKey = "data"; + const pivotDateGroups = new Map<number, Doc[]>(); + const docMap = new Map<Doc, ViewDefBounds>(); + const groupNames: ViewDefBounds[] = []; + const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field); + const curTime = toNumber(pivotDoc[fieldKey + "-timelineCur"]); + const curTimeSpan = Cast(pivotDoc[fieldKey + "-timelineSpan"], "number", null); + const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTime && (curTime - curTimeSpan); + const maxTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTime && (curTime + curTimeSpan); + const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3)); + const fontHeight = panelDim[1] > 58 ? 30 : panelDim[1] / 2; + const findStack = (time: number, stack: number[]) => { + const index = stack.findIndex(val => val === undefined || val < x); + return index === -1 ? stack.length : index; + }; + + let minTime = minTimeReq === undefined ? Number.MAX_VALUE : minTimeReq; + let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq; + filterDocs.map(doc => { + const num = NumCast(doc[timelineFieldKey], Number(StrCast(doc[timelineFieldKey]))); + if (!Number.isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) { + !pivotDateGroups.get(num) && pivotDateGroups.set(num, []); + pivotDateGroups.get(num)!.push(doc); + minTime = Math.min(num, minTime); + maxTime = Math.max(num, maxTime); } }); - return { elements: viewDefsToJSX(groupNames) }; + if (curTime !== undefined) { + if (curTime > maxTime || curTime - minTime > maxTime - curTime) { + maxTime = curTime + (curTime - minTime); + } else { + minTime = curTime - (maxTime - curTime); + } + } + setTimeout(() => { + pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime; + pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime; + }, 0); + + if (maxTime === minTime) { + maxTime = minTime + 1; + } + + const arrayofKeys = Array.from(pivotDateGroups.keys()); + const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2); + const scaling = panelDim[0] / (maxTime - minTime); + let x = 0; + let prevKey = Math.floor(minTime); + + if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) { + groupNames.push({ type: "text", text: toLabel(prevKey), x: x, y: 0, height: fontHeight, fontSize, payload: undefined }); + } + if (!sortedKeys.length && curTime !== undefined) { + groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined }); + } + + const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5); + const stacking: number[] = []; + let zind = 0; + sortedKeys.forEach(key => { + if (curTime !== undefined && curTime > prevKey && curTime <= key) { + groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: key }); + } + const keyDocs = pivotDateGroups.get(key)!; + x += scaling * (key - prevKey); + const stack = findStack(x, stacking); + prevKey = key; + if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) { + groupNames.push({ type: "text", text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); + } + layoutDocsAtTime(keyDocs, key); + }); + if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) { + x = (curTime - minTime) * scaling; + groupNames.push({ type: "text", text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined }); + } + if (Math.ceil(maxTime - minTime) * scaling > x + 25) { + groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); + } + + const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider], childDocs.filter(c => !filterDocs.includes(c))); + + function layoutDocsAtTime(keyDocs: Doc[], key: number) { + keyDocs.forEach(doc => { + const stack = findStack(x, stacking); + const layoutDoc = Doc.Layout(doc); + let wid = pivotAxisWidth; + let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + if (hgt > pivotAxisWidth) { + hgt = pivotAxisWidth; + wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + } + docMap.set(doc, { + type: "doc", + x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2, + zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt / (Math.max(stack, 1)), payload: undefined + }); + stacking[stack] = x + pivotAxisWidth; + }); + } +} + +function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>, + poolData: Map<string, PoolData>, viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], groupNames: ViewDefBounds[], minWidth: number, extras: ViewDefBounds[], + extraDocs: Doc[]): ViewDefResult[] { + + const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds); + const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as ViewDefBounds); + const aggBounds = aggregateBounds(docEles.concat(grpEles), 0, 0); + aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x); + const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); + let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale; + if (Number.isNaN(scale)) scale = 1; + + childPairs.filter(d => docMap.get(d.layout)).map(pair => { + const newPosRaw = docMap.get(pair.layout); + if (newPosRaw) { + const newPos = { + x: newPosRaw.x * scale, + y: newPosRaw.y * scale, + z: newPosRaw.z, + highlight: newPosRaw.highlight, + zIndex: newPosRaw.zIndex, + width: (newPosRaw.width || 0) * scale, + height: newPosRaw.height! * scale + }; + poolData.set(pair.layout[Id], { transition: "transform 1s", ...newPos }); + } + }); + extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 })); + + return viewDefsToJSX(extras.concat(groupNames).map(gname => ({ + type: gname.type, + text: gname.text, + x: gname.x * scale, + 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), + fontSize: gname.fontSize, + payload: gname.payload + }))); } export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index b8fbaef5c..1038347d4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -68,8 +68,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); const m = targetAhyperlink.getBoundingClientRect(); const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; - this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; + this.props.B.props.Document[bfield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; + this.props.B.props.Document[bfield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; }, 0); } }) @@ -95,10 +95,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const pt2 = [bpt.point.x, bpt.point.y]; const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - return !aActive && !bActive ? (null) : + const text = StrCast(this.props.A.props.Document.linkRelationship); + return !aActive && !bActive ? (null) : (<> + <text x={(pt1[0] + pt2[0]) / 2} y={(pt1[1] + pt2[1]) / 2}> + {text !== "-ungrouped-" ? text : ""} + </text> <line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} x1={`${pt1[0]}`} y1={`${pt1[1]}`} - x2={`${pt2[0]}`} y2={`${pt2[1]}`} />; + x2={`${pt2[0]}`} y2={`${pt2[1]}`} /> + </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index bb9ae4326..92fa2781c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -8,74 +8,65 @@ import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormView.scss"; import React = require("react"); import v5 = require("uuid/v5"); +import { computed } from "mobx"; +import { FieldResult } from "../../../../new_fields/Doc"; +import { List } from "../../../../new_fields/List"; @observer export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { - protected getCursors(): CursorField[] { + @computed protected get cursors(): CursorField[] { const doc = this.props.Document; - const id = CurrentUserUtils.id; - if (!id) { + let cursors: FieldResult<List<CursorField>>; + const { id } = CurrentUserUtils; + if (!id || !(cursors = Cast(doc.cursors, listSpec(CursorField)))) { return []; } - - const cursors = Cast(doc.cursors, listSpec(CursorField)); - const now = mobxUtils.now(); - // const now = Date.now(); - return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000); + return (cursors || []).filter(({ data: { metadata } }) => metadata.id !== id && (now - metadata.timestamp) < 1000); } - private crosshairs?: HTMLCanvasElement; - drawCrosshairs = (backgroundColor: string) => { - if (this.crosshairs) { - const ctx = this.crosshairs.getContext('2d'); - if (ctx) { - ctx.fillStyle = backgroundColor; - ctx.fillRect(0, 0, 20, 20); - - ctx.fillStyle = "black"; - ctx.lineWidth = 0.5; - - ctx.beginPath(); + @computed get renderedCursors() { + return this.cursors.map(({ data: { metadata, position: { x, y } } }) => { + return ( + <div key={metadata.id} className="collectionFreeFormRemoteCursors-cont" + style={{ transform: `translate(${x - 10}px, ${y - 10}px)` }} + > + <canvas className="collectionFreeFormRemoteCursors-canvas" + ref={(el) => { + if (el) { + const ctx = el.getContext('2d'); + if (ctx) { + ctx.fillStyle = "#" + v5(metadata.id, v5.URL).substring(0, 6).toUpperCase() + "22"; + ctx.fillRect(0, 0, 20, 20); - ctx.moveTo(10, 0); - ctx.lineTo(10, 8); + ctx.fillStyle = "black"; + ctx.lineWidth = 0.5; - ctx.moveTo(10, 20); - ctx.lineTo(10, 12); + ctx.beginPath(); - ctx.moveTo(0, 10); - ctx.lineTo(8, 10); + ctx.moveTo(10, 0); + ctx.lineTo(10, 8); - ctx.moveTo(20, 10); - ctx.lineTo(12, 10); + ctx.moveTo(10, 20); + ctx.lineTo(10, 12); - ctx.stroke(); + ctx.moveTo(0, 10); + ctx.lineTo(8, 10); - // ctx.font = "10px Arial"; - // ctx.fillText(Doc.CurrentUserEmail[0].toUpperCase(), 10, 10); - } - } - } + ctx.moveTo(20, 10); + ctx.lineTo(12, 10); - get sharedCursors() { - return this.getCursors().map(c => { - const m = c.data.metadata; - const l = c.data.position; - this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22"); - return ( - <div key={m.id} className="collectionFreeFormRemoteCursors-cont" - style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }} - > - <canvas className="collectionFreeFormRemoteCursors-canvas" - ref={(el) => { if (el) this.crosshairs = el; }} + ctx.stroke(); + } + } + }} width={20} height={20} /> <p className="collectionFreeFormRemoteCursors-symbol"> - {m.identifier[0].toUpperCase()} + {metadata.identifier[0].toUpperCase()} </p> </div> ); @@ -83,6 +74,6 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV } render() { - return this.sharedCursors; + return this.renderedCursors; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 2213b7882..730392ab5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -22,6 +22,8 @@ .collectionFreeform-customText { position: absolute; text-align: center; + overflow-y: auto; + overflow-x: hidden; } .collectionfreeformview-container { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 047a3a1cc..a73e601fd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,18 +3,20 @@ import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload, faTextHeight } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc"; +import { computedFn } from "mobx-utils"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkTool, InkField, InkData } from "../../../../new_fields/InkField"; -import { createSchema, makeInterface } from "../../../../new_fields/Schema"; +import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast, FieldValue } from "../../../../new_fields/Types"; +import { Cast, NumCast, ScriptCast, BoolCast, StrCast, FieldValue } 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, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; -import { Docs, DocUtils } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; +import { Docs } from "../../../documents/Documents"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; @@ -32,15 +34,12 @@ import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView } from "../CollectionSubView"; -import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; +import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { computedFn } from "mobx-utils"; -import { TraceMobx } from "../../../../new_fields/util"; -import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { RichTextField } from "../../../../new_fields/RichTextField"; import { List } from "../../../../new_fields/List"; @@ -59,8 +58,8 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, useClusters: "boolean", fitToBox: "boolean", - xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", @@ -83,6 +82,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; private _layoutPoolData = new ObservableMap<string, any>(); + private _cachedPool: Map<string, any> = new Map(); @observable private _pullCoords: number[] = [0, 0]; @observable private _pullDirection: string = ""; @@ -123,26 +123,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); } @action - onDrop = (e: React.DragEvent): Promise<void> => { + onExternalDrop = (e: React.DragEvent): Promise<void> => { const pt = this.getTransform().transformPoint(e.pageX, e.pageY); - return super.onDrop(e, { x: pt[0], y: pt[1] }); + return super.onExternalDrop(e, { x: pt[0], y: pt[1] }); } @undoBatch @action - drop = (e: Event, de: DragManager.DropEvent) => { + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (this.props.Document.isBackground) return false; const xf = this.getTransform(); const xfo = this.getTransformOverlay(); const [xp, yp] = xf.transformPoint(de.x, de.y); const [xpo, ypo] = xfo.transformPoint(de.x, de.y); - if (super.drop(e, de)) { + if (super.onInternalDrop(e, de)) { if (de.complete.docDragData) { if (de.complete.docDragData.droppedDocuments.length) { const firstDoc = de.complete.docDragData.droppedDocuments[0]; @@ -218,6 +219,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @undoBatch + @action updateClusters(useClusters: boolean) { this.props.Document.useClusters = useClusters; this._clusterSets.length = 0; @@ -255,7 +257,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); } childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); - childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString()); } } @@ -291,16 +292,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } getClusterColor = (doc: Doc) => { - let clusterColor = ""; + let clusterColor = this.props.backgroundColor?.(doc); const cluster = NumCast(doc.cluster); if (this.Document.useClusters) { if (this._clusterSets.length <= cluster) { setTimeout(() => this.updateCluster(doc), 0); } else { // choose a cluster color from a palette - const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; + const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; clusterColor = colors[cluster % colors.length]; - const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); + const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); @@ -527,19 +528,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let x = this.Document._panX || 0; let y = this.Document._panY || 0; - const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); + const 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) { + if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) { PDFMenu.Instance.fadeOut(true); - const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0; - const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0; - const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx; - const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny; - const ranges = docs.filter(doc => doc).reduce((range, doc) => { - const x = this.childDataProvider(doc).x;//NumCast(doc.x); - const y = this.childDataProvider(doc).y;//NumCast(doc.y); - const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width); - const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.height); + 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]]); @@ -844,7 +845,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - setScaleToZoom = (doc: Doc, scale: number = 0.5) => { + setScaleToZoom = (doc: Doc, scale: number = 0.75) => { this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } @@ -856,6 +857,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @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); getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -875,6 +877,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ContainingCollectionDoc: this.props.Document, focus: this.focusDocument, backgroundColor: this.getClusterColor, + backgroundHalo: this.backgroundHalo, parentActive: this.props.active, bringToFront: this.bringToFront, zoomToScale: this.zoomToScale, @@ -882,93 +885,159 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }; } - getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { + 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) { return { ...result, transition: "transform 1s" }; } const layoutDoc = Doc.Layout(params.doc); - return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") }; + const { x, y, z, color, zIndex } = params.doc; + return { + x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), + width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") + }; } - viewDefsToJSX = (views: any[]) => { + viewDefsToJSX = (views: ViewDefBounds[]) => { return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); } - private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { + onViewDefDivClick = (e: React.MouseEvent, payload: any) => { + (this.props.Document.onViewDefDivClick as ScriptField)?.script.run({ this: this.props.Document, payload }); + } + private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { + const { x, y, z } = viewDef; + const color = StrCast(viewDef.color); + const width = Cast(viewDef.width, "number"); + const height = Cast(viewDef.height, "number"); + const transform = `translate(${x}px, ${y}px)`; if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below - const x = Cast(viewDef.x, "number"); - const y = Cast(viewDef.y, "number"); - const z = Cast(viewDef.z, "number"); - const width = Cast(viewDef.width, "number"); - const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); - return [text, x, y, width, height].some(val => val === undefined) ? undefined : + return [text, x, y].some(val => val === undefined) ? undefined : { - ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> + ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z + color} style={{ width, height, color, fontSize, transform }}> {text} </div>, - bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + bounds: viewDef + }; + } else if (viewDef.type === "div") { + return [x, y].some(val => val === undefined) ? undefined : + { + ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)} + style={{ width, height, backgroundColor: color, transform }} />, + bounds: viewDef }; } } childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { - if (!doc) { - console.log(doc); - } return this._layoutPoolData.get(doc[Id]); }.bind(this)); - doPivotLayout(poolData: ObservableMap<string, any>) { - return computePivotLayout(poolData, this.props.Document, this.childDocs, - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); + doTimelineLayout(poolData: Map<string, PoolData>) { + return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } - doFreeformLayout(poolData: ObservableMap<string, any>) { + doPivotLayout(poolData: Map<string, PoolData>) { + return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); + } + + doFreeformLayout(poolData: Map<string, PoolData>) { const layoutDocs = this.childLayoutPairs.map(pair => pair.layout); const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); - let state = initResult && initResult.success ? initResult.result.scriptState : undefined; + const state = initResult && initResult.success ? initResult.result.scriptState : undefined; const elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : []; this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => { - const data = poolData.get(pair.layout[Id]); const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state }); - state = pos.state === undefined ? state : pos.state; - if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height || pos.transition !== data.transition) { - runInAction(() => poolData.set(pair.layout[Id], pos)); - } + poolData.set(pair.layout[Id], pos); }); - return { elements: elements }; + return elements; } - get doLayoutComputation() { - let computedElementData: { elements: ViewDefResult[] }; - switch (this.Document._freeformLayoutEngine) { - case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break; - default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break; + @computed get doInternalLayoutComputation() { + const newPool = new Map<string, any>(); + switch (this.props.layoutEngine?.()) { + case "timeline": return { newPool, computedElementData: this.doTimelineLayout(newPool) }; + case "pivot": return { newPool, computedElementData: this.doPivotLayout(newPool) }; + } + 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; } - this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair => - computedElementData.elements.push({ + 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; + runInAction(() => + Array.from(newPool.keys()).map(key => { + const lastPos = this._cachedPool.get(key); // last computed pos + const newPos = newPool.get(key); + if (!lastPos || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex || newPos.width !== lastPos.width || newPos.height !== lastPos.height) { + this._layoutPoolData.set(key, newPos); + } + })); + this._cachedPool.clear(); + Array.from(newPool.keys()).forEach(k => this._cachedPool.set(k, newPool.get(k))); + 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)} dataProvider={this.childDataProvider} + LayoutDoc={this.childLayoutDocFunc} jitterRotation={NumCast(this.props.Document.jitterRotation)} - fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />, + fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />, bounds: this.childDataProvider(pair.layout) })); - return computedElementData; + return elements; } componentDidMount() { super.componentDidMount(); - this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; }, - action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)), + this._layoutComputeReaction = reaction(() => this.doLayoutComputation, + (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); } componentWillUnmount() { - this._layoutComputeReaction && this._layoutComputeReaction(); + this._layoutComputeReaction?.(); } @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } elementFunc = () => this._layoutElements; @@ -980,23 +1049,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { - const docs = DocListCast(this.Document[this.props.fieldKey]); + const docs = this.childLayoutPairs; const startX = this.Document._panX || 0; let x = startX; let y = this.Document._panY || 0; let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc._width))); - const height = Math.max(...docs.map(doc => NumCast(doc._height))); - for (const doc of docs) { - doc.x = x; - doc.y = y; + const width = Math.max(...docs.map(doc => NumCast(doc.layout._width))); + const height = Math.max(...docs.map(doc => NumCast(doc.layout._height))); + docs.forEach(pair => { + pair.layout.x = x; + pair.layout.y = y; x += width + 20; if (++i === 6) { i = 0; x = startX; y += height + 20; } - } + }); }, "arrange contents"); } @@ -1044,6 +1113,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // } onContextMenu = (e: React.MouseEvent) => { + if (this.props.children && this.props.annotationsKey) return; const layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); @@ -1081,19 +1151,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } }); - layoutItems.push({ - description: "Add Note ...", - subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ - description: (i + 1) + ": " + StrCast(note.title), - event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })), - icon: "eye" - })) as ContextMenuProps[], - icon: "eye" - }); ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" }); } - private childViews = () => { const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; return [ @@ -1102,13 +1162,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ]; } - // @observable private _palette?: JSX.Element; - children = () => { const eles: JSX.Element[] = []; eles.push(...this.childViews()); - // this._palette && (eles.push(this._palette)); - // this.currentStroke && (eles.push(this.currentStroke)); eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); return eles; } @@ -1143,11 +1199,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey. // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document - // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale; return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, transform: this.contentScaling ? `scale(${this.contentScaling})` : "", @@ -1155,7 +1210,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { width: this.contentScaling ? `${100 / this.contentScaling}%` : "", height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? + {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> @@ -1182,7 +1237,7 @@ interface CollectionFreeFormOverlayViewProps { @observer class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps>{ render() { - return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); + return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index fb476b54b..d4f1a5444 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -3,25 +3,24 @@ import { observer } from "mobx-react"; import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc"; import { InkField, InkData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; -import { listSpec } from "../../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; +import { Cast, NumCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; -import { Docs } from "../../../documents/Documents"; +import { Docs, DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; +import { ContextMenu } from "../../ContextMenu"; import { PreviewCursor } from "../../PreviewCursor"; -import { CollectionViewType } from "../CollectionView"; +import { SubCollectionViewProps } from "../CollectionSubView"; +import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; -import { SubCollectionViewProps } from "../CollectionSubView"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { RichTextField } from "../../../../new_fields/RichTextField"; -import { InteractionUtils } from "../../../util/InteractionUtils"; +import { CollectionView } from "../CollectionView"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -69,7 +68,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque //make textbox and add it to this collection // tslint:disable-next-line:prefer-const let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); - if (e.key === "q" && e.ctrlKey) { + if (e.key === ":") { + DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y); + + ContextMenu.Instance.displayMenu(this._downX, this._downY); + } else if (e.key === "q" && e.ctrlKey) { e.preventDefault(); (async () => { const text: string = await navigator.clipboard.readText(); @@ -103,8 +106,9 @@ 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: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" })); + 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-" }); @@ -303,27 +307,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[], asTemplate: boolean) => { + getCollection = (selected: Doc[], asTemplate: boolean, isBackground: boolean = false) => { const bounds = this.Bounds; - const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", - "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; - const colorPalette = Cast(this.props.Document.colorPalette, listSpec("string")); - if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette); - const palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]); - const usedPaletted = new Map<string, number>(); - [...this.props.activeDocuments(), this.props.Document].map(child => { - const bg = StrCast(Doc.Layout(child).backgroundColor); - if (palette.indexOf(bg) !== -1) { - palette.splice(palette.indexOf(bg), 1); - if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); - else usedPaletted.set(bg, 1); - } - }); - usedPaletted.delete("#f1efeb"); - usedPaletted.delete("white"); - usedPaletted.delete("rgba(255,255,255,1)"); - const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); - const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; // const inkData = this.ink ? this.ink.inkData : undefined; const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; const newCollection = creator(selected, { @@ -331,8 +316,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y: bounds.top, _panX: 0, _panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + isBackground, + backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined, _width: bounds.width, _height: bounds.height, _LODdisable: true, @@ -358,7 +343,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return d; }); } - const newCollection = this.getCollection(selected, e.key === "t"); + const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t"); this.props.addDocument(newCollection); this.props.selectDocuments([newCollection], []); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -448,8 +433,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); - const newCollection = this.getCollection(selected); - selected.map(d => { this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; @@ -457,24 +440,24 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - newCollection._chromeStatus = "disabled"; - const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); - newCollection.x = bounds.left + bounds.width; - Doc.GetProto(newCollection).summaryDoc = summary; - Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); - if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - const container = Docs.Create.FreeformDocument([summary, newCollection], { - x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true, - _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-" - }); - Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" - this.props.addLiveTextDocument(container); - } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them - Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" - this.props.addLiveTextDocument(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: "-summary-" }); + const portal = Doc.MakeAlias(summary); + Doc.GetProto(summary)["data-annotations"] = new List<Doc>(selected); + Doc.GetProto(summary).layout_portal = CollectionView.LayoutString("data-annotations"); + summary._backgroundColor = "#e2ad32"; + portal.layoutKey = "layout_portal"; + DocUtils.MakeLink({ doc: summary, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, "portal link", "portal link"); + + this.props.addLiveTextDocument(summary); + MarqueeOptionsMenu.Instance.fadeOut(true); + } + @action + background = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const newCollection = this.getCollection([], false, true); + this.props.addDocument(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + setTimeout(() => this.props.selectDocuments([newCollection], []), 0); } @undoBatch @@ -490,7 +473,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") { + if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -498,10 +481,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (e.key === "c" || e.key === "t") { this.collection(e); } - if (e.key === "s" || e.key === "S") { this.summary(e); } + if (e.key === "b") { + this.background(e); + } this.cleanupInteractions(false); } } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index f57ba438a..821c8d804 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -1,12 +1,13 @@ .collectionMulticolumnView_contents { display: flex; + overflow: hidden; width: 100%; height: 100%; - overflow: hidden; .document-wrapper { display: flex; flex-direction: column; + width: 100%; .label-wrapper { display: flex; @@ -17,13 +18,13 @@ } - .resizer { + .multiColumnResizer { cursor: ew-resize; transition: 0.5s opacity ease; display: flex; flex-direction: column; - .internal { + .multiColumnResizer-hdl { width: 100%; height: 100%; transition: 0.5s background-color ease; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 041eb69da..82175c0b5 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,19 +1,19 @@ +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; -import { makeInterface } from '../../../../new_fields/Schema'; -import { documentSchema } from '../../../../new_fields/documentSchemas'; -import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import * as React from "react"; import { Doc } from '../../../../new_fields/Doc'; -import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../../new_fields/Types'; +import { DragManager } from '../../../util/DragManager'; +import { Transform } from '../../../util/Transform'; +import { undoBatch } from '../../../util/UndoManager'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; -import { Utils } from '../../../../Utils'; +import { CollectionSubView } from '../CollectionSubView'; import "./collectionMulticolumnView.scss"; -import { computed, trace, observable, action } from 'mobx'; -import { Transform } from '../../../util/Transform'; -import WidthLabel from './MulticolumnWidthLabel'; import ResizeBar from './MulticolumnResizer'; -import { undoBatch } from '../../../util/UndoManager'; -import { DragManager } from '../../../util/DragManager'; +import WidthLabel from './MulticolumnWidthLabel'; +import { List } from '../../../../new_fields/List'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; const MulticolumnDocument = makeInterface(documentSchema); @@ -28,13 +28,13 @@ interface LayoutData { starSum: number; } -export const WidthUnit = { +export const DimUnit = { Pixel: "px", Ratio: "*" }; -const resolvedUnits = Object.values(WidthUnit); -const resizerWidth = 4; +const resolvedUnits = Object.values(DimUnit); +const resizerWidth = 8; @observer export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { @@ -45,12 +45,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); } /** - * This loops through all childLayoutPairs and extracts the values for widthUnit - * and widthMagnitude, ignoring any that are malformed. Additionally, it then + * This loops through all childLayoutPairs and extracts the values for dimUnit + * and dimMagnitude, ignoring any that are malformed. Additionally, it then * normalizes the ratio values so that one * value is always 1, with the remaining * values proportionate to that easily readable metric. * @returns the list of the resolved width specifiers (unit and magnitude pairs) @@ -60,11 +60,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu private get resolvedLayoutInformation(): LayoutData { let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; - this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => { - const unit = StrCast(widthUnit); - const magnitude = NumCast(widthMagnitude); + this.childLayoutPairs.map(pair => { + const unit = StrCast(pair.layout.dimUnit, "*"); + const magnitude = NumCast(pair.layout.dimMagnitude, 1); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - (unit === WidthUnit.Ratio) && (starSum += magnitude); + (unit === DimUnit.Ratio) && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); } /** @@ -82,9 +82,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu setTimeout(() => { const { ratioDefinedDocs } = this; if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1))); if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1); } } }); @@ -103,7 +103,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu @computed private get totalFixedAllocation(): number | undefined { return this.resolvedLayoutInformation?.widthSpecifiers.reduce( - (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0); + (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); } /** @@ -119,7 +119,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu private get totalRatioAllocation(): number | undefined { const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length; if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { - return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._xMargin); } } @@ -160,8 +160,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu if (columnUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let width = NumCast(layout.widthMagnitude); - if (StrCast(layout.widthUnit) === WidthUnit.Ratio) { + let width = NumCast(layout.dimMagnitude, 1); + if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { width *= columnUnitLength; } return width; @@ -190,11 +190,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu @undoBatch @action - drop = (e: Event, de: DragManager.DropEvent) => { - if (super.drop(e, de)) { + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (super.onInternalDrop(e, de)) { de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d.widthUnit = "*"; - d.widthMagnitude = 1; + d.dimUnit = "*"; + d.dimMagnitude = 1; })); } return false; @@ -203,32 +203,43 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + return <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + backgroundColor={this.props.backgroundColor} + CollectionDoc={this.props.Document} + PanelWidth={width} + PanelHeight={height} + getTransform={dxf} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + />; + } /** * @returns the resolved list of rendered child documents, displayed * at their resolved pixel widths, each separated by a resizer. */ @computed private get contents(): JSX.Element[] | null { - const { childLayoutPairs } = this; + // 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 { 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 height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div - className={"document-wrapper"} - key={Utils.GenerateGuid()} - > - <ContentFittingDocumentView - {...this.props} - Document={layout} - DataDocument={layout.resolvedDataDoc as Doc} - CollectionDoc={this.props.Document} - PanelWidth={() => this.lookupPixels(layout)} - PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)} - getTransform={() => this.lookupIndividualTransform(layout)} - onClick={this.onChildClickHandler} - /> + <div className={"document-wrapper"} + key={"wrapper" + i} + style={{ width: width() }} > + {this.getDisplayDoc(layout, dxf, width, height)} <WidthLabel layout={layout} collectionDoc={Document} @@ -236,7 +247,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu </div>, <ResizeBar width={resizerWidth} - key={Utils.GenerateGuid()} + key={"resizer" + i} columnUnitLength={this.getColumnUnitLength} toLeft={layout} toRight={childLayoutPairs[i + 1]?.layout} @@ -249,7 +260,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu render(): JSX.Element { return ( - <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}> + <div className={"collectionMulticolumnView_contents"} + style={{ + marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) + }} ref={this.createDashEventsTarget}> {this.contents} </div> ); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss new file mode 100644 index 000000000..79fb195e8 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -0,0 +1,35 @@ +.collectionMultirowView_contents { + display: flex; + overflow: hidden; + width: 100%; + height: 100%; + flex-direction: column; + + .document-wrapper { + display: flex; + flex-direction: row; + height: 100%; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .multiRowResizer { + cursor: ns-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: row; + + .multiRowResizer-hdl { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx new file mode 100644 index 000000000..5e59f8237 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -0,0 +1,272 @@ +import { observer } from 'mobx-react'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; +import * as React from "react"; +import { Doc } from '../../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../../Utils'; +import "./collectionMultirowView.scss"; +import { computed, trace, observable, action } from 'mobx'; +import { Transform } from '../../../util/Transform'; +import HeightLabel from './MultirowHeightLabel'; +import ResizeBar from './MultirowResizer'; +import { undoBatch } from '../../../util/UndoManager'; +import { DragManager } from '../../../util/DragManager'; + +type MultirowDocument = makeInterface<[typeof documentSchema]>; +const MultirowDocument = makeInterface(documentSchema); + +interface HeightSpecifier { + magnitude: number; + unit: string; +} + +interface LayoutData { + heightSpecifiers: HeightSpecifier[]; + starSum: number; +} + +export const DimUnit = { + Pixel: "px", + Ratio: "*" +}; + +const resolvedUnits = Object.values(DimUnit); +const resizerHeight = 8; + +@observer +export class CollectionMultirowView extends CollectionSubView(MultirowDocument) { + + /** + * @returns the list of layout documents whose width unit is + * *, denoting that it will be displayed with a ratio, not fixed pixel, value + */ + @computed + private get ratioDefinedDocs() { + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); + } + + /** + * This loops through all childLayoutPairs and extracts the values for dimUnit + * and dimUnit, ignoring any that are malformed. Additionally, it then + * normalizes the ratio values so that one * value is always 1, with the remaining + * values proportionate to that easily readable metric. + * @returns the list of the resolved width specifiers (unit and magnitude pairs) + * as well as the sum of the * coefficients, i.e. the ratio magnitudes + */ + @computed + private get resolvedLayoutInformation(): LayoutData { + let starSum = 0; + const heightSpecifiers: HeightSpecifier[] = []; + this.childLayoutPairs.map(pair => { + const unit = StrCast(pair.layout.dimUnit, "*"); + const magnitude = NumCast(pair.layout.dimMagnitude, 1); + if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { + (unit === DimUnit.Ratio) && (starSum += magnitude); + heightSpecifiers.push({ magnitude, unit }); + } + /** + * Otherwise, the child document is ignored and the remaining + * space is allocated as if the document were absent from the child list + */ + }); + + /** + * Here, since these values are all relative, adjustments during resizing or + * manual updating can, though their ratios remain the same, cause the values + * themselves to drift toward zero. Thus, whenever we change any of the values, + * we normalize everything (dividing by the smallest magnitude). + */ + setTimeout(() => { + const { ratioDefinedDocs } = this; + if (this.childLayoutPairs.length) { + const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout.dimMagnitude, 1))); + if (minimum !== 0) { + ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum); + } + } + }); + + return { heightSpecifiers, starSum }; + } + + /** + * This returns the total quantity, in pixels, that this + * view needs to reserve for child documents that have + * (with higher priority) requested a fixed pixel width. + * + * If the underlying resolvedLayoutInformation returns null + * because we're waiting on promises to resolve, this value will be undefined as well. + */ + @computed + private get totalFixedAllocation(): number | undefined { + return this.resolvedLayoutInformation?.heightSpecifiers.reduce( + (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0); + } + + /** + * @returns the total quantity, in pixels, that this + * view needs to reserve for child documents that have + * (with lower priority) requested a certain relative proportion of the + * remaining pixel width not allocated for fixed widths. + * + * If the underlying totalFixedAllocation returns undefined + * because we're waiting indirectly on promises to resolve, this value will be undefined as well. + */ + @computed + private get totalRatioAllocation(): number | undefined { + const layoutInfoLen = this.resolvedLayoutInformation.heightSpecifiers.length; + if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { + return this.props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._yMargin); + } + } + + /** + * @returns the total quantity, in pixels, that + * 1* (relative / star unit) is worth. For example, + * if the configuration has three documents, with, respectively, + * widths of 2*, 2* and 1*, and the panel width returns 1000px, + * this accessor returns 1000 / (2 + 2 + 1), or 200px. + * Elsewhere, this is then multiplied by each relative-width + * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). + * + * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined + * because we're waiting indirectly on promises to resolve, this value will be undefined as well. + */ + @computed + private get rowUnitLength(): number | undefined { + if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { + return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; + } + } + + /** + * This wrapper function exists to prevent mobx from + * needlessly rerendering the internal ContentFittingDocumentViews + */ + private getRowUnitLength = () => this.rowUnitLength; + + /** + * @param layout the document whose transform we'd like to compute + * Given a layout document, this function + * returns the resolved width it has requested, in pixels. + * @returns the stored row width if already in pixels, + * or the ratio width evaluated to a pixel value + */ + private lookupPixels = (layout: Doc): number => { + const rowUnitLength = this.rowUnitLength; + if (rowUnitLength === undefined) { + return 0; // we're still waiting on promises to resolve + } + let height = NumCast(layout.dimMagnitude, 1); + if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { + height *= rowUnitLength; + } + return height; + } + + /** + * @returns the transform that will correctly place + * the document decorations box, shifted to the right by + * the sum of all the resolved row widths of the + * documents before the target. + */ + private lookupIndividualTransform = (layout: Doc) => { + const rowUnitLength = this.rowUnitLength; + if (rowUnitLength === undefined) { + return Transform.Identity(); // we're still waiting on promises to resolve + } + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.props.ScreenToLocalTransform().translate(0, -offset); + } + offset += this.lookupPixels(candidate) + resizerHeight; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @undoBatch + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (super.onInternalDrop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.dimUnit = "*"; + d.dimMagnitude = 1; + })); + } + return false; + } + + + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + return <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + backgroundColor={this.props.backgroundColor} + CollectionDoc={this.props.Document} + PanelWidth={width} + PanelHeight={height} + getTransform={dxf} + onClick={this.onChildClickHandler} + renderDepth={this.props.renderDepth + 1} + />; + } + /** + * @returns the resolved list of rendered child documents, displayed + * at their resolved pixel widths, each separated by a resizer. + */ + @computed + private get contents(): JSX.Element[] | null { + const { childLayoutPairs } = this; + const { Document, PanelWidth } = 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 height = () => this.lookupPixels(layout); + const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + collector.push( + <div + className={"document-wrapper"} + key={"wrapper" + i} + > + {this.getDisplayDoc(layout, dxf, width, height)} + <HeightLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + height={resizerHeight} + key={"resizer" + i} + columnUnitLength={this.getRowUnitLength} + toTop={layout} + toBottom={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div className={"collectionMultirowView_contents"} + style={{ + width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, + height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, + marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), + marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) + }} ref={this.createDashEventsTarget}> + {this.contents} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 11e210958..2cbeb3526 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react"; import { observable, action } from "mobx"; import { Doc } from "../../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../../new_fields/Types"; -import { WidthUnit } from "./CollectionMulticolumnView"; +import { DimUnit } from "./CollectionMulticolumnView"; +import { UndoManager } from "../../../util/UndoManager"; interface ResizerProps { width: number; @@ -12,30 +13,24 @@ interface ResizerProps { toRight?: Doc; } -enum ResizeMode { - Global = "blue", - Pinned = "red", - Undefined = "black" -} - const resizerOpacity = 1; @observer export default class ResizeBar extends React.Component<ResizerProps> { @observable private isHoverActive = false; @observable private isResizingActive = false; - @observable private resizeMode = ResizeMode.Undefined; + private _resizeUndo?: UndoManager.Batch; @action - private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); e.preventDefault(); - this.resizeMode = mode; window.removeEventListener("pointermove", this.onPointerMove); window.removeEventListener("pointerup", this.onPointerUp); window.addEventListener("pointermove", this.onPointerMove); window.addEventListener("pointerup", this.onPointerUp); this.isResizingActive = true; + this._resizeUndo = UndoManager.StartBatch("multcol resizing"); } private onPointerMove = ({ movementX }: PointerEvent) => { @@ -46,14 +41,12 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const { widthUnit, widthMagnitude } = toNarrow; - const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; - toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementX) / scale); } - if (this.resizeMode === ResizeMode.Pinned && toWiden) { - const { widthUnit, widthMagnitude } = toWiden; - const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; - toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale; + if (toWiden) { + const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale); } } } @@ -61,17 +54,17 @@ export default class ResizeBar extends React.Component<ResizerProps> { private get isActivated() { const { toLeft, toRight } = this.props; if (toLeft && toRight) { - if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toLeft) { - if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toRight) { - if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { return false; } return true; @@ -81,17 +74,18 @@ export default class ResizeBar extends React.Component<ResizerProps> { @action private onPointerUp = () => { - this.resizeMode = ResizeMode.Undefined; this.isResizingActive = false; this.isHoverActive = false; window.removeEventListener("pointermove", this.onPointerMove); window.removeEventListener("pointerup", this.onPointerUp); + this._resizeUndo?.end(); + this._resizeUndo = undefined; } render() { return ( <div - className={"resizer"} + className={"multiColumnResizer"} style={{ width: this.props.width, opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 @@ -99,16 +93,7 @@ export default class ResizeBar extends React.Component<ResizerProps> { onPointerEnter={action(() => this.isHoverActive = true)} onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} > - <div - className={"internal"} - onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} - style={{ backgroundColor: this.resizeMode }} - /> - <div - className={"internal"} - onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} - style={{ backgroundColor: this.resizeMode }} - /> + <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> </div> ); } diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx index b394fed62..5b2054428 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -4,7 +4,7 @@ import { computed } from "mobx"; import { Doc } from "../../../../new_fields/Doc"; import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; import { EditableView } from "../../EditableView"; -import { WidthUnit } from "./CollectionMulticolumnView"; +import { DimUnit } from "./CollectionMulticolumnView"; interface WidthLabelProps { layout: Doc; @@ -18,8 +18,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { @computed private get contents() { const { layout, decimals } = this.props; - const getUnit = () => StrCast(layout.widthUnit); - const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3)); + const getUnit = () => StrCast(layout.dimUnit); + const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3)); return ( <div className={"label-wrapper"}> <EditableView @@ -27,7 +27,7 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { SetValue={value => { const converted = Number(value); if (!isNaN(converted) && converted > 0) { - layout.widthMagnitude = converted; + layout.dimMagnitude = converted; return true; } return false; @@ -37,8 +37,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> { <EditableView GetValue={getUnit} SetValue={value => { - if (Object.values(WidthUnit).includes(value)) { - layout.widthUnit = value; + if (Object.values(DimUnit).includes(value)) { + layout.dimUnit = value; return true; } return false; diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx new file mode 100644 index 000000000..899577fd5 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { DimUnit } from "./CollectionMultirowView"; + +interface HeightLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class HeightLabel extends React.Component<HeightLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.dimUnit); + const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3)); + return ( + <div className={"label-wrapper"}> + <EditableView + GetValue={getMagnitude} + SetValue={value => { + const converted = Number(value); + if (!isNaN(converted) && converted > 0) { + layout.dimMagnitude = converted; + return true; + } + return false; + }} + contents={getMagnitude()} + /> + <EditableView + GetValue={getUnit} + SetValue={value => { + if (Object.values(DimUnit).includes(value)) { + layout.dimUnit = value; + return true; + } + return false; + }} + contents={getUnit()} + /> + </div> + ); + } + + render() { + return BoolCast(this.props.collectionDoc.showHeightLabels) ? this.contents : (null); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx new file mode 100644 index 000000000..9df8cc3e2 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -0,0 +1,101 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { DimUnit } from "./CollectionMultirowView"; +import { UndoManager } from "../../../util/UndoManager"; + +interface ResizerProps { + height: number; + columnUnitLength(): number | undefined; + toTop?: Doc; + toBottom?: Doc; +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + private _resizeUndo?: UndoManager.Batch; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + this._resizeUndo = UndoManager.StartBatch("multcol resizing"); + } + + private onPointerMove = ({ movementY }: PointerEvent) => { + const { toTop: toTop, toBottom: toBottom, columnUnitLength } = this.props; + const movingDown = movementY > 0; + const toNarrow = movingDown ? toBottom : toTop; + const toWiden = movingDown ? toTop : toBottom; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementY) / scale); + } + if (toWiden) { + const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementY) / scale); + } + } + } + + private get isActivated() { + const { toTop, toBottom } = this.props; + if (toTop && toBottom) { + if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toTop) { + if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toBottom) { + if (StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + this._resizeUndo?.end(); + this._resizeUndo = undefined; + } + + render() { + return ( + <div + className={"multiRowResizer"} + style={{ + height: this.props.height, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div className={"multiRowResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> + </div> + ); + } + +}
\ No newline at end of file |
