diff options
| author | Bob Zeleznik <zzzman@gmail.com> | 2020-02-08 13:48:11 -0500 |
|---|---|---|
| committer | Bob Zeleznik <zzzman@gmail.com> | 2020-02-08 13:48:11 -0500 |
| commit | 90d7fb57a64011763ad1d608126eacb052061e43 (patch) | |
| tree | fd96990ebd0ffe38f2285fbbceca942c1fb45587 /src/client/views/collections | |
| parent | e310c0fdcef6ac71ee492470d4ac689cbb094167 (diff) | |
| parent | 1b046f76cf39f1f6cb1875aa84b45db74b6d994e (diff) | |
Merge branch 'master' into monika_animation
Diffstat (limited to 'src/client/views/collections')
45 files changed, 4221 insertions, 1595 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss new file mode 100644 index 000000000..4815f1a59 --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -0,0 +1,40 @@ + +.collectionCarouselView-outer { + background: gray; + .collectionCarouselView-caption { + margin-left: 10%; + margin-right: 10%; + height: 50; + display: inline-block; + width: 80%; + } + .collectionCarouselView-image { + height: calc(100% - 50px); + display: inline-block; + width: 100%; + } +} +.carouselView-back { + position: absolute; + display: flex; + left: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +} +.carouselView-fwd { + position: absolute; + display: flex; + right: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx new file mode 100644 index 000000000..00edf71dd --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -0,0 +1,79 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observable, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../new_fields/Schema'; +import { NumCast, StrCast } from '../../../new_fields/Types'; +import { DragManager } from '../../util/DragManager'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import "./CollectionCarouselView.scss"; +import { CollectionSubView } from './CollectionSubView'; +import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { Doc } from '../../../new_fields/Doc'; +import { FormattedTextBox } from '../nodes/FormattedTextBox'; + +type CarouselDocument = makeInterface<[typeof documentSchema,]>; +const CarouselDocument = makeInterface(documentSchema); + +@observer +export class CollectionCarouselView extends CollectionSubView(CarouselDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + private _dropDisposer?: DragManager.DragDropDisposer; + + componentWillUnmount() { + this._dropDisposer && this._dropDisposer(); + } + + componentDidMount() { + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + } + } + + advance = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length; + } + goback = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + + panelHeight = () => this.props.PanelHeight() - 50; + @computed get content() { + const index = NumCast(this.layoutDoc._itemIndex); + return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : + <div> + <div className="collectionCarouselView-image"> + <ContentFittingDocumentView {...this.props} + Document={this.childLayoutPairs[index].layout} + DataDocument={this.childLayoutPairs[index].data} + PanelHeight={this.panelHeight} + getTransform={this.props.ScreenToLocalTransform} /> + </div> + <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}> + <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox> + </div> + </div> + } + @computed get buttons() { + return <> + <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}> + <FontAwesomeIcon icon={faCaretLeft} size={"2x"} /> + </div> + <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}> + <FontAwesomeIcon icon={faCaretRight} size={"2x"} /> + </div> + </>; + } + render() { + return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget}> + {this.content} + {this.buttons} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index bcdc9c97e..f518ef8fb 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -25,7 +25,7 @@ position: absolute; top: 0; left: 0; - overflow: hidden; + // overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu) .collectionDockingView-dragAsDocument { touch-action: none; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 75d92105b..cb413b3e3 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -20,7 +20,7 @@ import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, U import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; +import { DragManager } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from "../../util/UndoManager"; @@ -32,6 +32,10 @@ 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'; +import { PresElementBox } from '../presentationview/PresElementBox'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -39,7 +43,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) { + public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number, libraryPath?: Doc[]) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -47,7 +51,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : "" + dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : "", + libraryPath: libraryPath ? libraryPath.map(d => d[Id]) : [] //collectionDockingView: CollectionDockingView.Instance } }; @@ -95,14 +100,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public OpenFullScreen(docView: DocumentView) { - let document = Doc.MakeAlias(docView.props.Document); - let dataDoc = docView.props.DataDoc; - let newItemStackConfig = { + 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)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] }; - var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); + const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); @@ -113,7 +118,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } public CloseFullScreen = () => { - let target = this._goldenLayout._maximisedItem; + const target = this._goldenLayout._maximisedItem; if (target !== null && this._maximizedSrc) { this._goldenLayout._maximisedItem.remove(); SelectionManager.SelectDoc(this._maximizedSrc, false); @@ -128,26 +133,26 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public static CloseRightSplit(document: Doc): boolean { + public static CloseRightSplit(document: Opt<Doc>): boolean { if (!CollectionDockingView.Instance) return false; - let instance = CollectionDockingView.Instance; + 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) && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { + ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) || + (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) { child.contentItems[0].remove(); instance.layoutChanged(document); return true; } else { Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) || + (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)))) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); - let docs = Cast(instance.props.Document.data, listSpec(Doc)); - docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); return true; } return false; @@ -170,41 +175,63 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); } - - public Has = (document: Doc) => { - let docs = Cast(this.props.Document.data, listSpec(Doc)); - if (!docs) { - return false; + @undoBatch + @action + public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, 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.isDisplayPanel) { + const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath); + child.addChild(newItemStackConfig, undefined); + !addToSplit && child.contentItems[0].remove(); + instance.layoutChanged(document); + return true; + } + 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, dataDoc, undefined, libraryPath); + child.addChild(newItemStackConfig, undefined); + !addToSplit && child.contentItems[j].remove(); + instance.layoutChanged(document); + return true; + } + return false; + }); + }); + } + if (retVal) { + instance.stateChanged(); } - return docs.includes(document); + return retVal; } + // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split // @undoBatch @action - public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) { + public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { if (!CollectionDockingView.Instance) return false; - let instance = CollectionDockingView.Instance; - let docs = Cast(instance.props.Document.data, listSpec(Doc)); - if (docs) { - docs.push(document); - } - let newItemStackConfig = { + const instance = CollectionDockingView.Instance; + const newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] }; - var newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); if (instance._goldenLayout.root.contentItems.length === 0) { instance._goldenLayout.root.addChild(newContentItem); } else if (instance._goldenLayout.root.contentItems[0].isRow) { instance._goldenLayout.root.contentItems[0].addChild(newContentItem); } else { - var collayout = instance._goldenLayout.root.contentItems[0]; - var newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); collayout.parent.replaceChild(collayout, newRow); newRow.addChild(newContentItem, undefined, true); @@ -213,25 +240,28 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp collayout.config.width = 50; newContentItem.config.width = 50; } - if (minimize) { - // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes - // newContentItem.config.width = 10; - // newContentItem.config.height = 10; - } newContentItem.callDownwards('_$init'); instance.layoutChanged(); return true; } + // + // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // @undoBatch @action - public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => { - Doc.GetProto(document).lastOpened = new DateField; - let docs = Cast(this.props.Document.data, listSpec(Doc)); - if (docs) { - docs.push(document); + public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[], shiftKey?: boolean) { + document.isDisplayPanel = true; + if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath, shiftKey)) { + CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath); } - let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); + } + + @undoBatch + @action + public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => { + Doc.GetProto(document).lastOpened = new DateField; + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument, undefined, libraryPath); if (stack === undefined) { let stack: any = this._goldenLayout.root; while (!stack.isStack) { @@ -254,7 +284,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } setupGoldenLayout() { - var config = StrCast(this.props.Document.dockingConfig); + const config = StrCast(this.props.Document.dockingConfig); if (config) { if (!this._goldenLayout) { runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); @@ -292,13 +322,13 @@ 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, // we will leak a GoldenLayout, because we try to destroy it before we ever create it setTimeout(() => this.setupGoldenLayout(), 1); - let userDoc = CurrentUserUtils.UserDocument; + const userDoc = CurrentUserUtils.UserDocument; userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false); this.props.Document.workspaceBrush = true; } @@ -329,7 +359,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } @action onResize = (event: any) => { - var cur = this._containerRef.current; + const cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); @@ -348,36 +378,43 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { this._isPointerDown = true; - let onPointerUp = action(() => { + const onPointerUp = action(() => { window.removeEventListener("pointerup", onPointerUp); this._isPointerDown = false; }); window.addEventListener("pointerup", onPointerUp); - var className = (e.target as any).className; + const className = (e.target as any).className; if (className === "messageCounter") { e.stopPropagation(); e.preventDefault(); - let x = e.clientX; - let y = e.clientY; - let docid = (e.target as any).DashDocId; - let tab = (e.target as any).parentElement as HTMLElement; + 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) && DragLinksAsDocuments(tab, x, y, sourceDoc))); + (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; } } + updateDataField = async (json: string) => { + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + + if (docids) { + const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); + Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); + } + } + @undoBatch stateChanged = () => { - let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); - CollectionDockingView.Instance._removedDocs.map(theDoc => - docs && docs.indexOf(theDoc) !== -1 && - docs.splice(docs.indexOf(theDoc), 1)); - CollectionDockingView.Instance._removedDocs.length = 0; - var json = JSON.stringify(this._goldenLayout.toConfig()); + const json = JSON.stringify(this._goldenLayout.toConfig()); this.props.Document.dockingConfig = json; + this.updateDataField(json); + if (this.undohack && !this.hack) { this.undohack.end(); this.undohack = undefined; @@ -391,7 +428,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } htmlToElement(html: string) { - var template = document.createElement('template'); + const template = document.createElement('template'); html = html.trim(); // Never return a text node of whitespace as the result template.innerHTML = html; return template.content.firstChild; @@ -403,24 +440,24 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.contentItem.parent.config.fixed = true; } - let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; - let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; + 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) { - let dragSpan = document.createElement("span"); + const dragSpan = document.createElement("span"); dragSpan.style.position = "relative"; dragSpan.style.bottom = "6px"; dragSpan.style.paddingLeft = "4px"; dragSpan.style.paddingRight = "2px"; - let gearSpan = document.createElement("span"); + const gearSpan = document.createElement("span"); gearSpan.style.position = "relative"; gearSpan.style.paddingLeft = "0px"; gearSpan.style.paddingRight = "12px"; - let upDiv = document.createElement("span"); + 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) => { if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return; - var activeContentItem = tab.header.parent.getActiveContentItem(); + const activeContentItem = tab.header.parent.getActiveContentItem(); if (tab.contentItem !== activeContentItem) { tab.header.parent.setActiveContentItem(tab.contentItem); } @@ -428,20 +465,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp }; ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" - onPointerDown={ - e => { - e.preventDefault(); - e.stopPropagation(); - DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { - handlers: { dragComplete: emptyFunction }, - hideSource: false - }); - }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); + onPointerDown={e => { + e.preventDefault(); + e.stopPropagation(); + const dragData = new DragManager.DocumentDragData([doc]); + dragData.dropAction = doc.dropAction === "alias" ? "alias" : doc.dropAction === "copy" ? "copy" : undefined; + DragManager.StartDocumentDrag([dragSpan], dragData, e.clientX, e.clientY); + }}> + <FontAwesomeIcon icon="file" size="lg" /> + </span>, dragSpan); ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan); - // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => { - // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); - // return true; - // }} />, upDiv); tab.reactComponents = [dragSpan, gearSpan, upDiv]; tab.element.append(dragSpan); tab.element.append(gearSpan); @@ -458,12 +491,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.closeElement.off('click') //unbind the current click handler .click(async function () { tab.reactionDisposer && tab.reactionDisposer(); - let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); + const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); if (doc instanceof Doc) { - let theDoc = doc; + const theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); - let userDoc = CurrentUserUtils.UserDocument; + const userDoc = CurrentUserUtils.UserDocument; let recent: Doc | undefined; if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); @@ -490,9 +523,31 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); + this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); } }); + + // starter code for bezel to add new pane + // stack.element.on("touchstart", (e: TouchEvent) => { + // if (e.targetTouches.length === 2) { + // let pt1 = e.targetTouches.item(0); + // let pt2 = e.targetTouches.item(1); + // let threshold = 40 * window.devicePixelRatio; + // if (pt1 && pt2 && InteractionUtils.TwoPointEuclidist(pt1, pt2) < threshold) { + // let edgeThreshold = 30 * window.devicePixelRatio; + // let center = InteractionUtils.CenterPoint([pt1, pt2]); + // let stackRect: DOMRect = stack.element.getBoundingClientRect(); + // let nearLeft = center.X - stackRect.x < edgeThreshold; + // let nearTop = center.Y - stackRect.y < edgeThreshold; + // let nearRight = stackRect.right - center.X < edgeThreshold; + // let nearBottom = stackRect.bottom - center.Y < edgeThreshold; + // let ns = [nearLeft, nearTop, nearRight, nearBottom].filter(n => n); + // if (ns.length === 1) { + + // } + // } + // } + // }); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler .click(action(async function () { @@ -500,13 +555,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.remove(); stack.contentItems.forEach(async (contentItem: any) => { - let doc = await DocServer.GetRefField(contentItem.config.props.documentId); + const doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { let recent: Doc | undefined; if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); } - let theDoc = doc; + const theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); } }); @@ -516,7 +571,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp .off('click') //unbind the current click handler .click(action(function () { stack.config.fixed = !stack.config.fixed; - // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); + // const url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); })); } @@ -543,11 +598,13 @@ interface DockedFrameProps { documentId: FieldId; dataDocumentId: FieldId; glContainer: any; + libraryPath: (FieldId[]); //collectionDockingView: CollectionDockingView } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { _mainCont: HTMLDivElement | null = null; + @observable private _libraryPath: Doc[] = []; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @@ -565,6 +622,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); } })); + this.props.libraryPath && this.setupLibraryPath(); + } + + async setupLibraryPath() { + Promise.all(this.props.libraryPath.map(async docid => { + const d = await DocServer.GetRefField(docid); + return d instanceof Doc ? d : undefined; + })).then(action((list: (Doc | undefined)[]) => this._libraryPath = list.filter(d => d).map(d => d as Doc))); } /** @@ -572,28 +637,35 @@ 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 - let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; + const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; if (curPres) { - let 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, undefined); } } } + /** + * 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() { - let observer = new _global.ResizeObserver(action((entries: any) => { - for (let entry of entries) { + const observer = new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { this._panelWidth = entry.contentRect.width; this._panelHeight = entry.contentRect.height; } @@ -618,63 +690,64 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get layoutDoc() { return this._document && Doc.Layout(this._document); } - panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; + panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; contentScaling = () => { if (this.layoutDoc!.type === DocumentType.PDF) { - if ((this.layoutDoc && this.layoutDoc.fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) { - return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth); + if ((this.layoutDoc && this.layoutDoc._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); } else { - return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight); + return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); } } const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); if (!nativeW || !nativeH) return 1; - let wscale = this.panelWidth() / nativeW; + const wscale = this.panelWidth() / nativeW; return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; } ScreenToLocalTransform = () => { if (this._mainCont && this._mainCont.children) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); - scale = Utils.GetScreenTransform(this._mainCont).scale; + const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); + const scale = Utils.GetScreenTransform(this._mainCont).scale; return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); } 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; } - addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => { + addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); if (doc.dockingConfig) { - MainView.Instance.openWorkspace(doc); - return true; + return MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, dataDoc); + return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); } else { - return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); + return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc, libraryPath); } } @computed get docView() { + TraceMobx(); if (!this._document) return (null); const document = this._document; - let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; + const resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; return <DocumentView key={document[Id]} + LibraryPath={this._libraryPath} Document={document} DataDoc={resolvedDataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} - ruleProvider={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -685,7 +758,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { focus={emptyFunction} backgroundColor={returnEmptyString} addDocTab={this.addDocTab} - pinToPres={this.PinDoc} + pinToPres={DockedFrameRenderer.PinDoc} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} @@ -697,9 +770,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%" + height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", + width: this.widthpercent }}> {this.docView} </div >); } -}
\ No newline at end of file +} +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss new file mode 100644 index 000000000..eae9e0220 --- /dev/null +++ b/src/client/views/collections/CollectionLinearView.scss @@ -0,0 +1,75 @@ +@import "../globalCssVariables"; +@import "../_nodeModuleOverrides"; + +.collectionLinearView-outer{ + overflow: hidden; + height:100%; + .collectionLinearView { + display:flex; + height: 100%; + >label { + background: $dark-color; + color: $light-color; + display: inline-block; + border-radius: 18px; + font-size: 12.5px; + width: 18px; + height: 18px; + margin-top:auto; + margin-bottom:auto; + margin-right: 3px; + cursor: pointer; + transition: transform 0.2s; + } + + label p { + padding-left:5px; + } + + label:hover { + background: $main-accent; + transform: scale(1.15); + } + + >input { + display: none; + } + >input:not(:checked)~.collectionLinearView-content { + display: none; + } + + >input:checked~label { + transform: rotate(45deg); + transition: transform 0.5s; + cursor: pointer; + } + + .collectionLinearView-content { + display: flex; + opacity: 1; + position: relative; + margin-top: auto; + + .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { + position:relative; + margin:auto; + margin-left: 3px; + transform-origin: center 80%; + } + .collectionLinearView-docBtn-scalable:hover { + transform: scale(1.15); + } + + .collectionLinearView-round-button { + width: 18px; + height: 18px; + border-radius: 18px; + font-size: 15px; + } + + .collectionLinearView-round-button:hover { + transform: scale(1.15); + } + } + } +} diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx new file mode 100644 index 000000000..67062ae41 --- /dev/null +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -0,0 +1,133 @@ +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { makeInterface } from '../../../new_fields/Schema'; +import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { Transform } from '../../util/Transform'; +import "./CollectionLinearView.scss"; +import { CollectionViewType } from './CollectionView'; +import { CollectionSubView } from './CollectionSubView'; +import { DocumentView } from '../nodes/DocumentView'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { ScriptField } from '../../../new_fields/ScriptField'; + + +type LinearDocument = makeInterface<[typeof documentSchema,]>; +const LinearDocument = makeInterface(documentSchema); + +@observer +export class CollectionLinearView extends CollectionSubView(LinearDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; + private _dropDisposer?: DragManager.DragDropDisposer; + private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; + + componentWillUnmount() { + this._dropDisposer && this._dropDisposer(); + this._widthDisposer && this._widthDisposer(); + this._selectedDisposer && this._selectedDisposer(); + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + }); + } + + componentDidMount() { + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). + this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + { fireImmediately: true } + ); + + this._selectedDisposer = reaction( + () => NumCast(this.props.Document.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log); + } + }), + { fireImmediately: true } + ); + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + } + } + + public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + + dimension = () => NumCast(this.props.Document._height); // 2 * the padding + getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { + if (!ele.current) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + + render() { + const guid = Utils.GenerateGuid(); + return <div className="collectionLinearView-outer"> + <div className="collectionLinearView" ref={this.createDashEventsTarget} > + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} + onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> + <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> + + <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25) }}> + {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + const nested = pair.layout._viewType === CollectionViewType.Linear; + const dref = React.createRef<HTMLDivElement>(); + const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); + const deltaSize = nativeWidth * .15 / 2; + return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} + style={{ + width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, + height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + }} > + <DocumentView + Document={pair.layout} + DataDoc={pair.data} + LibraryPath={this.props.LibraryPath} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + removeDocument={this.props.removeDocument} + onClick={undefined} + ScreenToLocalTransform={this.getTransform(dref)} + ContentScaling={returnOne} + PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing + PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne}> + </DocumentView> + </div>; + })} + </div> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 52ebfafd3..e25a2f5eb 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 } from "mobx"; +import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Doc } from "../../../new_fields/Doc"; @@ -20,7 +20,6 @@ import { anchorPoints, Flyout } from "../DocumentDecorations"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; -import { undo } from "prosemirror-history"; library.add(faPalette); @@ -57,7 +56,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } @@ -65,9 +64,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr if (this._collapsed) { this.props.setDocHeight(this._heading, 20); } else { - let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header - let transformScale = this.props.screenToLocalTransform().Scale; - let trueHeight = rawHeight * transformScale; + const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header + const transformScale = this.props.screenToLocalTransform().Scale; + const trueHeight = rawHeight * transformScale; this.props.setDocHeight(this._heading, trueHeight); } } @@ -75,19 +74,19 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createAliasSelected = false; - if (de.data instanceof DragManager.DocumentDragData) { + if (de.complete.docDragData) { (this.props.parent.Document.dropConverter instanceof ScriptField) && - this.props.parent.Document.dropConverter.script.run({ dragData: de.data }); - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let castedValue = this.getValue(this._heading); - de.data.droppedDocuments.forEach(d => d[key] = castedValue); + this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); + 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); e.stopPropagation(); } }); getValue = (value: string): any => { - let parsed = parseInt(value); + const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; if (value.toLowerCase().indexOf("true") > -1) return true; if (value.toLowerCase().indexOf("false") > -1) return false; @@ -97,8 +96,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headingChanged = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let castedValue = this.getValue(value); + const key = StrCast(this.props.parent.props.Document.sectionFilter); + const castedValue = this.getValue(value); if (castedValue) { if (this.props.parent.sectionHeaders) { if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { @@ -136,18 +135,18 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action addDocument = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value }); + const key = StrCast(this.props.parent.props.Document.sectionFilter); + const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value }); newDoc[key] = this.getValue(this.props.heading); return this.props.parent.props.addDocument(newDoc); } deleteRow = undoBatch(action(() => { this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); + 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) { - let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); + const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); this.props.parent.sectionHeaders.splice(index, 1); } })); @@ -163,19 +162,17 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } startDrag = (e: PointerEvent) => { - let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + 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) { - let alias = Doc.MakeAlias(this.props.parent.props.Document); - let key = StrCast(this.props.parent.props.Document.sectionFilter); + 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; - let script = `return doc.${key} === ${value}`; - let compiled = CompileScript(script, { params: { doc: Doc.name } }); + const script = `return doc.${key} === ${value}`; + const compiled = CompileScript(script, { params: { doc: Doc.name } }); if (compiled.compiled) { - let scriptField = new ScriptField(compiled); - alias.viewSpecScript = scriptField; - let dragData = new DragManager.DocumentDragData([alias]); - DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); + alias.viewSpecScript = new ScriptField(compiled); + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); } e.stopPropagation(); @@ -197,7 +194,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr e.stopPropagation(); e.preventDefault(); - let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); + const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); this._startDragPosition = { x: dx, y: dy }; if (this._createAliasSelected) { @@ -210,17 +207,17 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } renderColorPicker = () => { - let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - let pink = PastelSchemaPalette.get("pink2"); - let purple = PastelSchemaPalette.get("purple4"); - let blue = PastelSchemaPalette.get("bluegreen1"); - let yellow = PastelSchemaPalette.get("yellow4"); - let red = PastelSchemaPalette.get("red2"); - let green = PastelSchemaPalette.get("bluegreen7"); - let cyan = PastelSchemaPalette.get("bluegreen5"); - let orange = PastelSchemaPalette.get("orange1"); - let gray = "#f1efeb"; + const pink = PastelSchemaPalette.get("pink2"); + const purple = PastelSchemaPalette.get("purple4"); + const blue = PastelSchemaPalette.get("bluegreen1"); + const yellow = PastelSchemaPalette.get("yellow4"); + const red = PastelSchemaPalette.get("red2"); + const green = PastelSchemaPalette.get("bluegreen7"); + const cyan = PastelSchemaPalette.get("bluegreen5"); + const orange = PastelSchemaPalette.get("orange1"); + const gray = "#f1efeb"; return ( <div className="collectionStackingView-colorPicker"> @@ -243,7 +240,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr toggleVisibility = action(() => this._collapsed = !this._collapsed); renderMenu = () => { - let selected = this._createAliasSelected; + const selected = this._createAliasSelected; return (<div className="collectionStackingView-optionPicker"> <div className="optionOptions"> <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> @@ -258,47 +255,66 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } } - - render() { - let 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)))); - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let heading = this._heading; - let style = this.props.parent; - let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - let headerEditableViewProps = { - GetValue: () => evContents, - SetValue: this.headingChanged, - contents: evContents, - oneLine: true, + @computed get contentLayout() { + const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); + const style = this.props.parent; const collapsed = this._collapsed; + const chromeStatus = this.props.parent.props.Document._chromeStatus; + const newEditableViewProps = { + GetValue: () => "", + SetValue: this.addDocument, + contents: "+ NEW", HeadingObject: this.props.headingObject, HeadingsHack: this._headingsHack, toggle: this.toggleVisibility, color: this._color }; - let newEditableViewProps = { - GetValue: () => "", - SetValue: this.addDocument, - contents: "+ NEW", + return collapsed ? (null) : + <div style={{ position: "relative" }}> + <div className={`collectionStackingView-masonryGrid`} + ref={this._contRef} + style={{ + padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`, + width: this.props.parent.NodeWidth, + gridGap: this.props.parent.gridGap, + gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), + }}> + {this.props.parent.children(this.props.docList)} + {this.props.parent.columnDragger} + </div> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / style.numGroupColumns }}> + <EditableView {...newEditableViewProps} /> + </div> : null + } + </div>; + } + + @computed get headingView() { + const heading = this._heading; + const key = StrCast(this.props.parent.props.Document.sectionFilter); + const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; + const headerEditableViewProps = { + GetValue: () => evContents, + SetValue: this.headingChanged, + contents: evContents, + oneLine: true, HeadingObject: this.props.headingObject, HeadingsHack: this._headingsHack, toggle: this.toggleVisibility, color: this._color }; - let headingView = this.props.parent.props.Document.miniHeaders ? - <div className="collectionStackingView-miniHeader" style={{ width: "100%" }}> - {<EditableView {...headerEditableViewProps} />} + return this.props.parent.props.Document.miniHeaders ? + <div className="collectionStackingView-miniHeader"> + <EditableView {...headerEditableViewProps} /> </div> : - this.props.headingObject ? + !this.props.headingObject ? (null) : <div className="collectionStackingView-sectionHeader" ref={this._headerRef} > <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} - style={{ - width: "100%", - background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", - color: "grey" - }}> - {<EditableView {...headerEditableViewProps} />} + style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", }}> + <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> @@ -321,47 +337,26 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </div> } </div> - </div > : (null); + </div>; + } + render() { const background = this._background; //to account for observables in Measure - const collapsed = this._collapsed; - let chromeStatus = this.props.parent.props.Document.chromeStatus; - return ( - <Measure offset onResize={this.handleResize}> - {({ measureRef }) => { - return <div ref={measureRef}> - <div className="collectionStackingView-masonrySection" - key={heading = "empty"} - style={{ width: this.props.parent.NodeWidth, background }} - ref={this.createRowDropRef} - onPointerEnter={this.pointerEnteredRow} - onPointerLeave={this.pointerLeaveRow} - > - {headingView} - {collapsed ? (null) : - < div style={{ position: "relative" }}> - <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`} - ref={this._contRef} - style={{ - padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`, - width: this.props.parent.NodeWidth, - gridGap: this.props.parent.gridGap, - gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), - }}> - {this.props.parent.children(this.props.docList)} - {this.props.parent.columnDragger} - </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> - </div> : null - } - </div> - } - </div > - </div>; - }} - </Measure> - ); + const contentlayout = this.contentLayout; + const headingview = this.headingView; + return <Measure offset onResize={this.handleResize}> + {({ measureRef }) => { + return <div ref={measureRef}> + <div className="collectionStackingView-masonrySection" + style={{ width: this.props.parent.NodeWidth, background }} + ref={this.createRowDropRef} + onPointerEnter={this.pointerEnteredRow} + onPointerLeave={this.pointerLeaveRow} + > + {headingview} + {contentlayout} + </div > + </div>; + }} + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 54a36f691..caffa7eb1 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,7 +1,7 @@ import React = require("react"); -import { action, computed, observable, trace, untracked, toJS } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, Column } from "react-table"; +import { CellInfo } from "react-table"; import "react-table/react-table.css"; import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc"; @@ -9,7 +9,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; +import { MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; @@ -37,7 +37,7 @@ export interface CellProps { renderDepth: number; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - moveDocument: (document: Doc, targetCollection: Doc, 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; @@ -89,8 +89,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // this._isEditing = true; // this.props.setIsEditing(true); - let field = this.props.rowProps.original[this.props.rowProps.column.id!]; - let doc = FieldValue(Cast(field, Doc)); + const field = this.props.rowProps.original[this.props.rowProps.column.id!]; + const doc = FieldValue(Cast(field, Doc)); if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); } @@ -105,13 +105,13 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } private drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - let fieldKey = this.props.rowProps.column.id as string; - if (de.data.draggedDocuments.length === 1) { - this._document[fieldKey] = de.data.draggedDocuments[0]; + if (de.complete.docDragData) { + const fieldKey = this.props.rowProps.column.id as string; + if (de.complete.docDragData.draggedDocuments.length === 1) { + this._document[fieldKey] = de.complete.docDragData.draggedDocuments[0]; } else { - let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.data.draggedDocuments, {}); + const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); this._document[fieldKey] = coll; } e.stopPropagation(); @@ -121,7 +121,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { private dropRef = (ele: HTMLElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); } } @@ -138,13 +138,13 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // } renderCellWithType(type: string | undefined) { - let dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - let props: FieldViewProps = { + const props: FieldViewProps = { Document: this.props.rowProps.original, DataDoc: this.props.rowProps.original, + LibraryPath: [], fieldKey: this.props.rowProps.column.id as string, - ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, @@ -161,23 +161,22 @@ export class CollectionSchemaCell extends React.Component<CellProps> { ContentScaling: returnOne }; - let field = props.Document[props.fieldKey]; - let doc = FieldValue(Cast(field, Doc)); - let fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); + const field = props.Document[props.fieldKey]; + const doc = FieldValue(Cast(field, Doc)); + const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); - let onItemDown = (e: React.PointerEvent) => { - if (fieldIsDoc) { - SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, - this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); - } + const onItemDown = (e: React.PointerEvent) => { + fieldIsDoc && SetupDrag(this._focusRef, + () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, + this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc | undefined, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, + this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); }; - let onPointerEnter = (e: React.PointerEvent): void => { + const onPointerEnter = (e: React.PointerEvent): void => { if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) { dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; } }; - let onPointerLeave = (e: React.PointerEvent): void => { + const onPointerLeave = (e: React.PointerEvent): void => { dragRef.current!.className = "collectionSchemaView-cellContainer"; }; @@ -187,7 +186,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--"; if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--"; if (type === "document") { - let doc = FieldValue(Cast(field, Doc)); + const doc = FieldValue(Cast(field, Doc)); contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; } @@ -215,7 +214,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => { - let field = props.Document[props.fieldKey]; + const field = props.Document[props.fieldKey]; if (Field.IsField(field)) { return Field.toScriptString(field); } @@ -226,14 +225,14 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (value.startsWith(":=")) { return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); } - let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (!script.compiled) { return false; } return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); }} OnFillDown={async (value: string) => { - const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { DocListCast(this.props.Document[this.props.fieldKey]). forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run)); @@ -287,15 +286,15 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { @action toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { this._isChecked = e.target.checked; - let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); + const script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); if (script.compiled) { this.applyToDoc(this._document, this.props.row, this.props.col, script.run); } } render() { - let reference = React.createRef<HTMLDivElement>(); - let onItemDown = (e: React.PointerEvent) => { + const reference = React.createRef<HTMLDivElement>(); + const onItemDown = (e: React.PointerEvent) => { (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index d24f63fbb..92dc8780e 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -1,5 +1,5 @@ import React = require("react"); -import { action, computed, observable, trace, untracked } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import "./CollectionSchemaView.scss"; import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -7,10 +7,8 @@ import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Flyout, anchorPoints } from "../DocumentDecorations"; import { ColumnType } from "./CollectionSchemaView"; -import { emptyFunction } from "../../../Utils"; -import { contains } from "typescript-collections/dist/lib/arrays"; import { faFile } from "@fortawesome/free-regular-svg-icons"; -import { SchemaHeaderField, RandomPastel, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes); @@ -32,7 +30,7 @@ export interface HeaderProps { export class CollectionSchemaHeader extends React.Component<HeaderProps> { render() { - let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : + const icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify"; return ( <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}> @@ -139,7 +137,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> renderTypes = () => { if (this.props.typeConst) return <></>; - let type = this.props.columnField.type; + const type = this.props.columnField.type; return ( <div className="collectionSchema-headerMenu-group"> <label>Column type:</label> @@ -170,7 +168,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } renderSorting = () => { - let sort = this.props.columnField.desc; + const sort = this.props.columnField.desc; return ( <div className="collectionSchema-headerMenu-group"> <label>Sort by:</label> @@ -193,14 +191,14 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } renderColors = () => { - let selected = this.props.columnField.color; + const selected = this.props.columnField.color; - let pink = PastelSchemaPalette.get("pink2"); - let purple = PastelSchemaPalette.get("purple2"); - let blue = PastelSchemaPalette.get("bluegreen1"); - let yellow = PastelSchemaPalette.get("yellow4"); - let red = PastelSchemaPalette.get("red2"); - let gray = "#f1efeb"; + const pink = PastelSchemaPalette.get("pink2"); + const purple = PastelSchemaPalette.get("purple2"); + const blue = PastelSchemaPalette.get("bluegreen1"); + const yellow = PastelSchemaPalette.get("yellow4"); + const red = PastelSchemaPalette.get("red2"); + const gray = "#f1efeb"; return ( <div className="collectionSchema-headerMenu-group"> @@ -288,17 +286,16 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { } @undoBatch - @action onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { - let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || + 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) { this.onSelect(this._searchTerm); } else { - this._searchTerm = this._key; + this.setSearchTerm(this._key); } } } @@ -334,11 +331,11 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { renderOptions = (): JSX.Element[] | JSX.Element => { if (!this._isOpen) return <></>; - let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || + 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; - let options = keyOptions.map(key => { + const options = keyOptions.map(key => { return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; }); diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 274c8b6d1..153bbd410 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -1,18 +1,18 @@ import React = require("react"); -import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0, RowInfo } from "react-table"; +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 { SelectionManager } from "../../util/SelectionManager"; -import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue } from "../../../new_fields/Types"; import { ContextMenu } from "../ContextMenu"; import { action } from "mobx"; import { library } from '@fortawesome/fontawesome-svg-core'; import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocumentManager } from "../../util/DocumentManager"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; library.add(faGripVertical, faTrash); @@ -43,10 +43,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> { document.removeEventListener("pointermove", this.onPointerMove); } onDragMove = (e: PointerEvent): void => { - let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); - let before = x[0] < bounds[0]; + const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + const before = x[0] < bounds[0]; this._header!.current!.className = "collectionSchema-col-wrapper"; if (before) this._header!.current!.className += " col-before"; if (!before) this._header!.current!.className += " col-after"; @@ -56,39 +56,39 @@ export class MovableColumn extends React.Component<MovableColumnProps> { createColDropTarget = (ele: HTMLDivElement) => { this._colDropDisposer && this._colDropDisposer(); if (ele) { - this._colDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.colDrop.bind(this) } }); + this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this)); } } colDrop = (e: Event, de: DragManager.DropEvent) => { document.removeEventListener("pointermove", this.onDragMove, true); - let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); - let before = x[0] < bounds[0]; - if (de.data instanceof DragManager.ColumnDragData) { - this.props.reorderColumns(de.data.colKey, this.props.columnValue, before, this.props.allColumns); + const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); + const before = x[0] < bounds[0]; + if (de.complete.columnDragData) { + this.props.reorderColumns(de.complete.columnDragData.colKey, this.props.columnValue, before, this.props.allColumns); return true; } return false; } onPointerMove = (e: PointerEvent) => { - let onRowMove = (e: PointerEvent) => { + const onRowMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); - let dragData = new DragManager.ColumnDragData(this.props.columnValue); + const dragData = new DragManager.ColumnDragData(this.props.columnValue); DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y); }; - let onRowUp = (): void => { + const onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); }; if (e.buttons === 1) { - let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + 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) { document.removeEventListener("pointermove", this.onPointerMove); e.stopPropagation(); @@ -106,14 +106,14 @@ export class MovableColumn extends React.Component<MovableColumnProps> { @action onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { this._dragRef = ref; - let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); + const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); this._startDragPosition = { x: dx, y: dy }; document.addEventListener("pointermove", this.onPointerMove); } render() { - let reference = React.createRef<HTMLDivElement>(); + const reference = React.createRef<HTMLDivElement>(); return ( <div className="collectionSchema-col" ref={this.createColDropTarget}> @@ -152,10 +152,10 @@ export class MovableRow extends React.Component<MovableRowProps> { document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { - let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; + const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + const before = x[1] < bounds[1]; this._header!.current!.className = "collectionSchema-row-wrapper"; if (before) this._header!.current!.className += " row-above"; if (!before) this._header!.current!.className += " row-below"; @@ -165,7 +165,7 @@ export class MovableRow extends React.Component<MovableRowProps> { createRowDropTarget = (ele: HTMLDivElement) => { this._rowDropDisposer && this._rowDropDisposer(); if (ele) { - this._rowDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); + this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } @@ -173,38 +173,39 @@ export class MovableRow extends React.Component<MovableRowProps> { const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc)); if (!rowDoc) return false; - let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; + const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const rect = this._header!.current!.getBoundingClientRect(); + const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); + const before = x[1] < bounds[1]; - if (de.data instanceof DragManager.DocumentDragData) { + const docDragData = de.complete.docDragData; + if (docDragData) { e.stopPropagation(); - if (de.data.draggedDocuments[0] === rowDoc) return true; - let addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before); - let movedDocs = de.data.draggedDocuments; - return (de.data.dropAction || de.data.userDropAction) ? - de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) - : (de.data.moveDocument) ? - movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, rowDoc, addDocument) || added, false) - : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false); + if (docDragData.draggedDocuments[0] === rowDoc) return true; + const addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before); + const movedDocs = docDragData.draggedDocuments; + return (docDragData.dropAction || docDragData.userDropAction) ? + docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) + : (docDragData.moveDocument) ? + movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false) + : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false); } return false; } onRowContextMenu = (e: React.MouseEvent): void => { - let description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; + const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); } @undoBatch @action - move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { - let targetView = DocumentManager.Instance.getDocumentView(target); + move: DragManager.MoveFunction = (doc: Doc, targetCollection: Doc | undefined, addDoc) => { + const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); if (targetView && targetView.props.ContainingCollectionDoc) { - return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); + return doc !== targetCollection && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); } - return doc !== target && this.props.removeDoc(doc) && addDoc(doc); + return doc !== targetCollection && this.props.removeDoc(doc) && addDoc(doc); } render() { @@ -217,8 +218,8 @@ export class MovableRow extends React.Component<MovableRowProps> { const doc = FieldValue(Cast(original, Doc)); if (!doc) return <></>; - let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => doc, this.move); + const reference = React.createRef<HTMLDivElement>(); + const onItemDown = SetupDrag(reference, () => doc, this.move); 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 cb95dcbbc..a24140b48 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -15,6 +15,11 @@ display: flex; justify-content: space-between; flex-wrap: nowrap; + touch-action: none; + + div { + touch-action: none; + } .collectionSchemaView-tableContainer { @@ -34,9 +39,9 @@ cursor: col-resize; } - .documentView-node:first-child { - background: $light-color; - } + // .documentView-node:first-child { + // background: $light-color; + // } } .ReactTable { @@ -49,7 +54,7 @@ .rt-table { height: 100%; display: -webkit-inline-box; - direction: ltr; + direction: ltr; overflow: visible; } @@ -202,7 +207,7 @@ button.add-column { border-bottom: 1px solid lightgray; &:first-child { - padding-top : 0; + padding-top: 0; } &:last-child { @@ -231,7 +236,7 @@ button.add-column { transition: background-color 0.2s; &:hover { - background-color: $light-color-secondary; + background-color: $light-color-secondary; } &.active { @@ -267,7 +272,7 @@ button.add-column { overflow-y: scroll; position: absolute; top: 28px; - box-shadow: 0 10px 16px rgba(0,0,0,0.1); + box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); .key-option { background-color: $light-color; @@ -380,6 +385,7 @@ button.add-column { &.editing { padding: 0; + input { outline: 0; border: none; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 65856cad3..fa8be5177 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -94,11 +94,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @action onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont!.getBoundingClientRect(); - let minWidth = 40; - let maxWidth = 1000; - let movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; - let width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; + 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 @@ -136,14 +136,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewPanel() { - let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; + const layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; return <div ref={this.createTarget}> <ContentFittingDocumentView Document={layoutDoc} DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} + LibraryPath={this.props.LibraryPath} childDocs={this.childDocs} renderDepth={this.props.renderDepth} - ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} PanelWidth={this.previewWidth} PanelHeight={this.previewHeight} getTransform={this.getPreviewTransform} @@ -156,8 +156,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} /> </div>; } @@ -223,7 +221,7 @@ export interface SchemaTableProps { renderDepth: number; deleteDocument: (document: Doc) => boolean; addDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; active: (outsideReaction: boolean) => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; @@ -258,11 +256,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get childDocs() { if (this.props.childDocs) return this.props.childDocs; - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; return DocListCast(doc[this.props.fieldKey]); } set childDocs(docs: Doc[]) { - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; doc[this.props.fieldKey] = new List<Doc>(docs); } @@ -288,12 +286,12 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { - let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); - let columns: Column<Doc>[] = []; - let tableIsFocused = this.props.isFocused(this.props.Document); - let focusedRow = this._focusedCell.row; - let focusedCol = this._focusedCell.col; - let isEditable = !this._headerIsEditing; + const possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); + const columns: Column<Doc>[] = []; + const tableIsFocused = this.props.isFocused(this.props.Document); + const focusedRow = this._focusedCell.row; + const focusedCol = this._focusedCell.col; + const isEditable = !this._headerIsEditing; if (this.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) { columns.push( @@ -313,8 +311,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { ); } - let cols = this.columns.map(col => { - let header = <CollectionSchemaHeader + const cols = this.columns.map(col => { + const header = <CollectionSchemaHeader keyValue={col} possibleKeys={possibleKeys} existingKeys={this.columns.map(c => c.heading)} @@ -333,11 +331,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> { accessor: (doc: Doc) => doc ? doc[col.heading] : 0, id: col.heading, Cell: (rowProps: CellInfo) => { - let rowIndex = rowProps.index; - let columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!); - let isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; + const rowIndex = rowProps.index; + const columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!); + const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - let props: CellProps = { + const props: CellProps = { row: rowIndex, col: columnIndex, rowProps: rowProps, @@ -358,7 +356,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { getField: this.getField, }; - let colType = this.getColumnType(col); + const colType = this.getColumnType(col); if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />; if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />; if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />; @@ -384,9 +382,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { constructor(props: SchemaTableProps) { super(props); // convert old schema columns (list of strings) into new schema columns (list of schema header fields) - let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []); + const oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []); if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") { - let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); + const newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns); } } @@ -418,10 +416,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { if (!rowInfo || column) return {}; - let row = rowInfo.index; + const row = rowInfo.index; //@ts-ignore - let col = this.columns.map(c => c.heading).indexOf(column!.id); - let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); + const col = this.columns.map(c => c.heading).indexOf(column!.id); + const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); // TODO: editing border doesn't work :( return { style: { @@ -432,7 +430,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action onCloseCollection = (collection: Doc): void => { - let index = this._openCollections.findIndex(col => col === collection[Id]); + const index = this._openCollections.findIndex(col => col === collection[Id]); if (index > -1) this._openCollections.splice(index, 1); } @@ -450,7 +448,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action onKeyDown = (e: KeyboardEvent): void => { if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) { - let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; + const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); @@ -479,7 +477,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch createRow = () => { - let newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 }); + const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }); this.props.addDocument(newDoc); } @@ -498,7 +496,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch @action deleteColumn = (key: string) => { - let columns = this.columns; + const columns = this.columns; if (columns === undefined) { this.columns = new List<SchemaHeaderField>([]); } else { @@ -513,7 +511,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch @action changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { - let columns = this.columns; + const columns = this.columns; if (columns === undefined) { this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); } else { @@ -523,7 +521,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } else { const index = columns.map(c => c.heading).indexOf(oldKey); if (index > -1) { - let column = columns[index]; + const column = columns[index]; column.setHeading(newKey); columns[index] = column; this.columns = columns; @@ -554,8 +552,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { if (columnTypes.get(columnField.heading)) return; - let columns = this.columns; - let index = columns.indexOf(columnField); + const columns = this.columns; + const index = columns.indexOf(columnField); if (index > -1) { columnField.setType(NumCast(type)); columns[index] = columnField; @@ -575,8 +573,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch setColumnColor = (columnField: SchemaHeaderField, color: string): void => { - let columns = this.columns; - let index = columns.indexOf(columnField); + const columns = this.columns; + const index = columns.indexOf(columnField); if (index > -1) { columnField.setColor(color); columns[index] = columnField; @@ -589,10 +587,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { - let columns = [...columnsValues]; - let oldIndex = columns.indexOf(toMove); - let relIndex = columns.indexOf(relativeTo); - let newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex; + const columns = [...columnsValues]; + const oldIndex = columns.indexOf(toMove); + const relIndex = columns.indexOf(relativeTo); + const newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex; if (oldIndex === newIndex) return; @@ -603,17 +601,17 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch @action setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { - let columns = this.columns; - let index = columns.findIndex(c => c.heading === columnField.heading); - let column = columns[index]; + const columns = this.columns; + const index = columns.findIndex(c => c.heading === columnField.heading); + const column = columns[index]; column.setDesc(descending); columns[index] = column; this.columns = columns; } get documentKeys() { - let docs = this.childDocs; - let keys: { [key: string]: boolean } = {}; + const docs = this.childDocs; + const keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. @@ -626,10 +624,23 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return Array.from(Object.keys(keys)); } + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.Document.textwrappedSchemaRows = new List<string>([]); + } else { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } + } + @action toggleTextWrapRow = (doc: Doc): void => { - let textWrapped = this.textWrappedRows; - let index = textWrapped.findIndex(id => doc[Id] === id); + const textWrapped = this.textWrappedRows; + const index = textWrapped.findIndex(id => doc[Id] === id); index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]); @@ -638,13 +649,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get reactTable() { - let children = this.childDocs; - let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); - let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); - let expanded = {}; + const children = this.childDocs; + const hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); + const expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); + const expanded = {}; //@ts-ignore expandedRowsList.forEach(row => expanded[row] = true); - console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( + const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable style={{ position: "relative" }} @@ -668,10 +679,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } onResizedChange = (newResized: Resize[], event: any) => { - let columns = this.columns; + const columns = this.columns; newResized.forEach(resized => { - let index = columns.findIndex(c => c.heading === resized.id); - let column = columns[index]; + const index = columns.findIndex(c => c.heading === resized.id); + const column = columns[index]; column.setWidth(resized.value); columns[index] = column; }); @@ -680,7 +691,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); + // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }) } } @@ -688,16 +700,16 @@ export class SchemaTable extends React.Component<SchemaTableProps> { makeDB = async () => { let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; - let self = this; + const self = this; this.childDocs.map(doc => { csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; }); csv.substring(0, csv.length - 1); - let dbName = StrCast(this.props.Document.title); - let res = await Gateway.Instance.PostSchema(csv, dbName); + const dbName = StrCast(this.props.Document.title); + const res = await Gateway.Instance.PostSchema(csv, dbName); if (self.props.CollectionView && self.props.CollectionView.props.addDocument) { - let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); + const schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { //self.props.CollectionView.props.addDocument(schemaDoc, false); self.props.Document.schemaDoc = schemaDoc; @@ -706,7 +718,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } getField = (row: number, col?: number) => { - let docs = this.childDocs; + const docs = this.childDocs; row = row % docs.length; while (row < 0) row += docs.length; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 29178b909..293dc5414 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 { @@ -97,6 +98,7 @@ .collectionStackingView-columnDoc { display: inline-block; + margin: auto; } .collectionStackingView-masonryDoc { @@ -177,7 +179,9 @@ .collectionStackingView-sectionHeader-subCont { outline: none; border: 0px; - color: $light-color; + color: $light-color; + width: 100%; + color: grey; letter-spacing: 2px; font-size: 75%; transition: transform 0.2s; @@ -382,4 +386,4 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index be3bfca0a..d772ace23 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,17 +1,16 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CursorProperty } from "csstype"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import Switch from 'rc-switch'; -import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Doc, HeightSym, WidthSym, DataSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types"; -import { emptyFunction, Utils, numberRange } from "../../../Utils"; -import { DocumentType } from "../../documents/DocumentTypes"; +import { emptyFunction, Utils } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -22,8 +21,9 @@ import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewField import { CollectionSubView } from "./CollectionSubView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; -import { ScriptBox } from "../ScriptBox"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; +import { TraceMobx } from "../../../new_fields/util"; +import { CollectionViewType } from "./CollectionView"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -38,35 +38,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @observable _scroll = 0; // used to force the document decoration to update when scrolling @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } - @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } - @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } - @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } - @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } + @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); } + @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } + @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); } + @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } - childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } - - children(docs: Doc[]) { + children(docs: Doc[], columns?: number) { this._docXfs.length = 0; return docs.map((d, i) => { - let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d; - let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); - let height = () => this.getDocHeight(layoutDoc); - let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(layoutDoc, dref.current!); + const height = () => this.getDocHeight(d); + const width = () => this.widthScale * Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + const dref = React.createRef<HTMLDivElement>(); + const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); - let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); - let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)} + {this.getDisplayDoc(d, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)} </div>; }); } @@ -83,20 +79,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return new Map<SchemaHeaderField, Doc[]>(); } const sectionHeaders = this.sectionHeaders; - let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); this.filteredChildren.map(d => { - let sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object; + 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 - let parsed = parseInt(sectionValue.toString()); - let castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; + const parsed = parseInt(sectionValue.toString()); + const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; // look for if header exists already - let existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`)); + const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`)); if (existingHeader) { fields.get(existingHeader)!.push(d); } else { - let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`); + const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); sectionHeaders.push(newSchemaHeader); } @@ -107,28 +103,28 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { componentDidMount() { super.componentDidMount(); this._heightDisposer = reaction(() => { - if (this.props.Document.autoHeight) { - let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); + if (this.props.Document._autoHeight) { + const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); if (this.isStackingView) { - let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { - let r1 = Math.max(maxHght, + const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { + const r1 = Math.max(maxHght, (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { - let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); + const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); return val; }, this.yMargin)); return r1; }, 0); return res; } else { - let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); + const sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15)); } } return -1; }, (hgt: number) => { - let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; - doc && hgt > 0 && (Doc.Layout(doc).height = hgt); + const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + doc && hgt > 0 && (Doc.Layout(doc)._height = hgt); }, { fireImmediately: true } ); @@ -146,36 +142,30 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @action - moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => { return this.props.removeDocument(doc) && addDocument(doc); } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDropTarget(ele!); //so the whole grid is the drop target? - } - - overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {}; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { - let layoutDoc = Doc.Layout(doc); - let height = () => this.getDocHeight(doc); - let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); + const layoutDoc = Doc.Layout(doc); + const height = () => this.getDocHeight(doc); return <ContentFittingDocumentView Document={doc} DataDocument={dataDoc} - showOverlays={this.overlays} - renderDepth={this.props.renderDepth} - ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} + LibraryPath={this.props.LibraryPath} + renderDepth={this.props.renderDepth + 1} fitToBox={this.props.fitToBox} onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} PanelHeight={height} - getTransform={finalDxf} + getTransform={dxf} focus={this.props.focus} CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} CollectionView={this.props.CollectionView} @@ -185,24 +175,22 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} - previewScript={undefined}> + pinToPres={this.props.pinToPres}> </ContentFittingDocumentView>; } getDocHeight(d?: Doc) { if (!d) return 0; - let layoutDoc = Doc.Layout(d); - let nw = NumCast(layoutDoc.nativeWidth); - let nh = NumCast(layoutDoc.nativeHeight); + const layoutDoc = Doc.Layout(d); + 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) { - let aspect = nw && nh ? nh / nw : 1; - if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) { + const aspect = nw && nh ? nh / nw : 1; + if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -215,8 +203,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @action onDividerMove = (e: PointerEvent): void => { - let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - let delta = dragPos - this._columnStart; + 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); } @@ -229,7 +217,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @computed get columnDragger() { - return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px` }} > + return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} + style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }} > <FontAwesomeIcon icon={"arrows-alt-h"} /> </div>; } @@ -237,28 +226,29 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - let where = [de.x, de.y]; + const where = [de.x, de.y]; let targInd = -1; - let plusOne = false; - if (de.data instanceof DragManager.DocumentDragData) { + let plusOne = 0; + if (de.complete.docDragData) { this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; - plusOne = (where[1] > (pos[1] + pos1[1]) / 2 ? 1 : 0) ? true : false; + const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1; + plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); - } - if (super.drop(e, de)) { - let newDoc = de.data.droppedDocuments[0]; - let docs = this.childDocList; - if (docs) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - let srcInd = docs.indexOf(newDoc); - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + (plusOne ? 1 : 0), 0, newDoc); + if (super.drop(e, de)) { + const newDoc = de.complete.docDragData.droppedDocuments[0]; + const docs = this.childDocList; + if (docs) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + const srcInd = docs.indexOf(newDoc); + docs.splice(srcInd, 1); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, newDoc); + } } } return false; @@ -266,19 +256,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @undoBatch @action onDrop = async (e: React.DragEvent): Promise<void> => { - let where = [e.clientX, e.clientY]; + const where = [e.clientX, e.clientY]; let targInd = -1; this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; } }); super.onDrop(e, {}, () => { if (targInd !== -1) { - let newDoc = this.childDocs[this.childDocs.length - 1]; - let docs = this.childDocList; + const newDoc = this.childDocs[this.childDocs.length - 1]; + const docs = this.childDocList; if (docs) { docs.splice(docs.length - 1, 1); docs.splice(targInd, 0, newDoc); @@ -288,13 +278,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } headings = () => Array.from(this.Sections.keys()); sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { - let key = this.sectionFilter; + const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } - let cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, + const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionStackingViewFieldColumn key={heading ? heading.heading : ""} @@ -305,30 +295,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} />; } getDocTransform(doc: Doc, dref: HTMLDivElement) { if (!dref) return Transform.Identity(); - let y = this._scroll; // required for document decorations to update when the text box container is scrolled - let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); - let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); - let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform(). - translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)). - scale(NumCast(doc.width, 1) / this.columnWidth); + const y = this._scroll; // required for document decorations to update when the text box container is scrolled + const { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + const outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]()); + const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2; + const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0); + return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling); } sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { - let key = this.sectionFilter; + const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } - let rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, + const rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); return <CollectionMasonryViewFieldRow key={heading ? heading.heading : ""} @@ -339,7 +330,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} setDocHeight={this.setDocHeight} />; @@ -355,61 +346,78 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { - let descending = BoolCast(this.props.Document.stackingHeadersSortDescending); - let firstEntry = descending ? b : a; - let secondEntry = descending ? a : b; + const descending = BoolCast(this.props.Document.stackingHeadersSortDescending); + const firstEntry = descending ? b : a; + const secondEntry = descending ? a : b; return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } onToggle = (checked: Boolean) => { - this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode"; } 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()) { - let subItems: ContextMenuProps[] = []; + 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" }); - - let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); - let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } + @computed get renderedSections() { + TraceMobx(); + let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; + if (this.sectionFilter) { + const entries = Array.from(this.Sections.entries()); + sections = entries.sort(this.sortFunc); + } + return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1])); + } + @computed get contentScale() { + const heightExtra = this.heightPercent > 1 ? this.heightPercent : 1; + return Math.min(this.props.Document[WidthSym]() / this.props.PanelWidth(), heightExtra * this.props.Document[HeightSym]() / this.props.PanelHeight()); + } + @computed get widthScale() { + return this.heightPercent < 1 ? Math.max(1, this.contentScale) : 1; + } + @computed get heightPercent() { + return this.props.PanelHeight() / this.layoutDoc[HeightSym](); + } render() { - let editableViewProps = { + TraceMobx(); + const editableViewProps = { GetValue: () => "", SetValue: this.addGroup, contents: "+ ADD A GROUP" }; - let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; - if (this.sectionFilter) { - let entries = Array.from(this.Sections.entries()); - sections = entries.sort(this.sortFunc); - } return ( <div className="collectionStackingMasonry-cont" > <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} ref={this.createRef} + style={{ + overflowY: this.props.active() ? "auto" : "hidden", + transform: `scale(${Math.min(1, this.heightPercent)})`, + height: `${100 * Math.max(1, this.contentScale)}%`, + width: `${100 * this.widthScale}%`, + transformOrigin: "top left", + }} onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => e.stopPropagation()} > - {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} + onWheel={e => this.props.active() && e.stopPropagation()} > + {this.renderedSections} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.Document.chromeStatus !== 'disabled' ? <Switch + {this.props.Document._chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.Document.chromeStatus !== 'view-mode'} + defaultChecked={this.props.Document._chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index b9d334b10..21982f1ca 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,20 +2,22 @@ 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, trace, runInAction } from "mobx"; +import { action, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, WidthSym } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; +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 { Utils } from "../../../Utils"; +import { ImageField } from "../../../new_fields/URLField"; +import { TraceMobx } from "../../../new_fields/util"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import { anchorPoints, Flyout } from "../DocumentDecorations"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; @@ -53,28 +55,28 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC this._dropRef = ele; this.dropDisposer && this.dropDisposer(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.columnDrop.bind(this) } }); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } } @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { this._createAliasSelected = false; - if (de.data instanceof DragManager.DocumentDragData) { - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let castedValue = this.getValue(this._heading); + if (de.complete.docDragData) { + const key = StrCast(this.props.parent.props.Document.sectionFilter); + const castedValue = this.getValue(this._heading); if (castedValue) { - de.data.droppedDocuments.forEach(d => d[key] = castedValue); + de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); } else { - de.data.droppedDocuments.forEach(d => d[key] = undefined); + de.complete.docDragData.droppedDocuments.forEach(d => d[key] = undefined); } this.props.parent.drop(e, de); e.stopPropagation(); } }); getValue = (value: string): any => { - let parsed = parseInt(value); + const parsed = parseInt(value); if (!isNaN(parsed)) { return parsed; } @@ -90,8 +92,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action headingChanged = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let castedValue = this.getValue(value); + const key = StrCast(this.props.parent.props.Document.sectionFilter); + const castedValue = this.getValue(value); if (castedValue) { if (this.props.parent.sectionHeaders) { if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { @@ -134,12 +136,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action addDocument = (value: string, shiftDown?: boolean) => { + if (!value) return false; this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); + 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); - let maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); - let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; + const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; return this.props.parent.props.addDocument(newDoc); } @@ -147,10 +150,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action deleteColumn = () => { this._createAliasSelected = false; - let key = StrCast(this.props.parent.props.Document.sectionFilter); + 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) { - let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); + const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); this.props.parent.sectionHeaders.splice(index, 1); } } @@ -166,10 +169,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } startDrag = (e: PointerEvent) => { - let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + 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) { - let alias = Doc.MakeAlias(this.props.parent.props.Document); - let key = StrCast(this.props.parent.props.Document.sectionFilter); + 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 }); @@ -195,7 +198,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC e.stopPropagation(); e.preventDefault(); - let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); + const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); this._startDragPosition = { x: dx, y: dy }; if (this._createAliasSelected) { @@ -208,17 +211,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } renderColorPicker = () => { - let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - - let pink = PastelSchemaPalette.get("pink2"); - let purple = PastelSchemaPalette.get("purple4"); - let blue = PastelSchemaPalette.get("bluegreen1"); - let yellow = PastelSchemaPalette.get("yellow4"); - let red = PastelSchemaPalette.get("red2"); - let green = PastelSchemaPalette.get("bluegreen7"); - let cyan = PastelSchemaPalette.get("bluegreen5"); - let orange = PastelSchemaPalette.get("orange1"); - let gray = "#f1efeb"; + const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + + const pink = PastelSchemaPalette.get("pink2"); + const purple = PastelSchemaPalette.get("purple4"); + const blue = PastelSchemaPalette.get("bluegreen1"); + const yellow = PastelSchemaPalette.get("yellow4"); + const red = PastelSchemaPalette.get("red2"); + const green = PastelSchemaPalette.get("bluegreen7"); + const cyan = PastelSchemaPalette.get("bluegreen5"); + const orange = PastelSchemaPalette.get("orange1"); + const gray = "#f1efeb"; return ( <div className="collectionStackingView-colorPicker"> @@ -243,7 +246,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } renderMenu = () => { - let selected = this._createAliasSelected; + const selected = this._createAliasSelected; return ( <div className="collectionStackingView-optionPicker"> <div className="optionOptions"> @@ -255,23 +258,73 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @observable private collapsed: boolean = false; - private toggleVisibility = action(() => { - this.collapsed = !this.collapsed; - }); + private toggleVisibility = action(() => this.collapsed = !this.collapsed); @observable _headingsHack: number = 1; + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + + const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; + Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => + docItems.push({ + description: ":" + fieldKey, event: () => { + const created = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document)); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.props.addDocument(created); + } + }, icon: "compress-arrows-alt" + })); + Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey => + docItems.push({ + description: ":" + fieldKey, event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.props.addDocument(created); + } + }, icon: "compress-arrows-alt" + })); + layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + + ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.setDefaultItem("::", (name: string): void => { + Doc.GetProto(this.props.parent.props.Document)[name] = ""; + const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + this.props.parent.props.addDocument(created); + } + }); + const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); + ContextMenu.Instance.displayMenu(x, y); + } + render() { - let cols = this.props.cols(); - let key = StrCast(this.props.parent.props.Document.sectionFilter); + TraceMobx(); + const cols = this.props.cols(); + const key = StrCast(this.props.parent.props.Document.sectionFilter); let templatecols = ""; - let headings = this.props.headings(); - let heading = this._heading; - let style = this.props.parent; - let singleColumn = style.isStackingView; - let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); - let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - let headerEditableViewProps = { + const headings = this.props.headings(); + const heading = this._heading; + const style = this.props.parent; + const singleColumn = style.isStackingView; + 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 = { GetValue: () => evContents, SetValue: this.headingChanged, contents: evContents, @@ -281,7 +334,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC toggle: this.toggleVisibility, color: this._color }; - let newEditableViewProps = { + const newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, contents: "+ NEW", @@ -290,12 +343,12 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC toggle: this.toggleVisibility, color: this._color }; - let headingView = this.props.headingObject ? + const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -335,7 +388,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - let chromeStatus = this.props.parent.props.Document.chromeStatus; + const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> @@ -354,13 +407,13 @@ 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') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> + <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> } diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 40e860b12..8c7e113b2 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -2,7 +2,7 @@ import { CollectionSubView } from "./CollectionSubView"; import { Transform } from "../../util/Transform"; import React = require("react"); import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx"; -import { Doc, HeightSym } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { NumCast } from "../../../new_fields/Types"; import "./CollectionStaffView.scss"; import { observer } from "mobx-react"; @@ -23,18 +23,14 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) { this.props.Document.staves = 5; } - @computed get fieldExtensionDoc() { - return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); - } - @computed get addStaffButton() { return <div onPointerDown={this.addStaff}>+</div>; } @computed get staves() { - let staves = []; + const staves = []; for (let i = 0; i < this._staves; i++) { - let rows = []; + const rows = []; for (let j = 0; j < 5; j++) { rows.push(<div key={`staff-${i}-${j}`} className="collectionStaffView-line"></div>); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a2675c9a3..262c60f17 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; @@ -6,9 +6,8 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, StrCast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -21,13 +20,17 @@ import { CollectionView } from "./CollectionView"; import React = require("react"); import { DocComponent } from "../DocComponent"; var path = require('path'); +import { basename } from 'path'; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; +import { Networking } from "../../Network"; +import { GestureUtils } from "../../../pen-gestures/GestureUtils"; +import { InteractionUtils } from "../../util/InteractionUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; removeDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; VisibleHeight?: () => number; @@ -38,61 +41,76 @@ export interface CollectionViewProps extends FieldViewProps { export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; - ruleProvider: Doc | undefined; children?: never | (() => JSX.Element[]) | React.ReactNode; isAnnotationOverlay?: boolean; annotationsKey: string; + layoutEngine?: () => string; } -export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { +export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; + private gestureDisposer?: GestureUtils.GestureEventDisposer; + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this.dropDisposer && this.dropDisposer(); + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this.dropDisposer?.(); + this.gestureDisposer?.(); + this.multiTouchDisposer?.(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); + this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view - this.createDropTarget(ele); + this.createDashEventsTarget(ele); } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - if (args[1] instanceof Doc) { - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent")); + this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc)?.[Id]], + (args) => { + const childLayout = Cast(this.props.Document.childLayout, Doc); + if (childLayout instanceof Doc) { + this.childDocs.map(doc => { + doc.layout_fromParent = childLayout; + doc.layoutKey = "layout_fromParent"; + }); } - else if (!(args[1] instanceof Promise)) { - this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout")); + else if (!(childLayout instanceof Promise)) { + this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout")); } - }); + }, { fireImmediately: true }); } componentWillUnmount() { this._childLayoutDisposer && this._childLayoutDisposer(); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey]; + const { annotationsKey, fieldKey } = this.props; + if (annotationsKey) { + return this.dataDoc[fieldKey + "-" + annotationsKey]; + } + return this.dataDoc[fieldKey]; } - get childLayoutPairs() { - return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! })); + get childLayoutPairs(): { layout: Doc; data: Doc; }[] { + const { Document, DataDoc } = this.props; + const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !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() { return Cast(this.dataField, listSpec(Doc)); } get childDocs() { - let docs = DocListCast(this.dataField); + const docs = DocListCast(this.dataField); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @@ -100,10 +118,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @action protected async setCursorPosition(position: [number, number]) { let ind; - let doc = this.props.Document; - let id = CurrentUserUtils.id; - let email = Doc.CurrentUserEmail; - let pos = { x: position[0], y: position[1] }; + const doc = this.props.Document; + const id = CurrentUserUtils.id; + const email = Doc.CurrentUserEmail; + const pos = { x: position[0], y: position[1] }; if (id && email) { const proto = Doc.GetProto(doc); if (!proto) { @@ -123,47 +141,49 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { cursors[ind].setPosition(pos); } else { - let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos }); + const entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos }); cursors.push(entry); } } } @undoBatch + protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { + + } + + @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { + const docDragData = de.complete.docDragData; (this.props.Document.dropConverter instanceof ScriptField) && - this.props.Document.dropConverter.script.run({ dragData: de.data }); - if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { - if (de.mods === "AltKey" && de.data.draggedDocuments.length) { - this.childDocs.map(doc => - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent")); + 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.layout_fromParent = docDragData.draggedDocuments[0]; + doc.layoutKey = "layout_fromParent"; + }); e.stopPropagation(); return true; } let added = false; - if (de.data.dropAction || de.data.userDropAction) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); - } else if (de.data.moveDocument) { - let movedDocs = de.data.draggedDocuments; + 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) => - de.data.droppedDocuments[i] !== d ? this.props.addDocument(de.data.droppedDocuments[i]) : - de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false); + docDragData.droppedDocuments[i] !== d ? this.props.addDocument(docDragData.droppedDocuments[i]) : + docDragData.moveDocument?.(d, this.props.Document, this.props.addDocument) || added, false); } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => { - let moved = this.props.addDocument(d); - return moved || added; - }, false); + added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } e.stopPropagation(); return added; } - else if (de.data instanceof DragManager.AnnotationDragData) { + else if (de.complete.annoDragData) { e.stopPropagation(); - return this.props.addDocument(de.data.dropDocument); + return this.props.addDocument(de.complete.annoDragData.dropDocument); } return false; } @@ -175,8 +195,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; } - let html = e.dataTransfer.getData("text/html"); - let text = e.dataTransfer.getData("text/plain"); + const html = e.dataTransfer.getData("text/html"); + const text = e.dataTransfer.getData("text/plain"); if (text && text.startsWith("<div")) { return; @@ -185,9 +205,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { e.preventDefault(); if (html && FormattedTextBox.IsFragment(html)) { - let href = FormattedTextBox.GetHref(html); + const href = FormattedTextBox.GetHref(html); if (href) { - let docid = FormattedTextBox.GetDocFromUrl(href); + const docid = FormattedTextBox.GetDocFromUrl(href); if (docid) { // prosemirror text containing link to dash document DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { @@ -196,27 +216,32 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options)); + this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text })); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); } return; } if (html && !html.startsWith("<a")) { - let tags = html.split("<"); + const tags = html.split("<"); if (tags[0] === "") tags.splice(0, 1); - let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; + const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; if (img) { - let split = img.split("src=\"")[1].split("\"")[0]; - let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); + const split = img.split("src=\"")[1].split("\"")[0]; + let source = split; + if (split.startsWith("data:image") && split.includes("base64")) { + const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [split] }); + source = Utils.prepend(clientAccessPath); + } + const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 }); ImageUtils.ExtractExif(doc); this.props.addDocument(doc); return; } else { - let path = window.location.origin + "/doc/"; + const path = window.location.origin + "/doc/"; if (text.startsWith(path)) { - let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0]; + 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 @@ -224,7 +249,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text }); this.props.addDocument(htmlDoc); } return; @@ -232,13 +257,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } if (text && text.indexOf("www.youtube.com/watch") !== -1) { const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); + this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 })); return; } let matches: RegExpExecArray | null; if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { - let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); - let proto = newBox.proto!; + 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..."; @@ -255,59 +280,61 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); console.log(mediaItems); } - let batch = UndoManager.StartBatch("collection view drop"); - let promises: Promise<void>[] = []; + 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++) { - const upload = window.location.origin + RouteStore.upload; - let item = e.dataTransfer.items[i]; + const item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.indexOf("uri") !== -1) { let str: string; - let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) + const prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) .then(action((s: string) => rp.head(Utils.CorsProxy(str = s)))) .then(result => { - let type = result["content-type"]; + const type = result["content-type"]; if (type) { - Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 }) + Docs.Get.DocumentFromType(type, str, options) .then(doc => doc && this.props.addDocument(doc)); } }); promises.push(prom); } - let type = item.type; + const type = item.type; if (item.kind === "file") { - let file = item.getAsFile(); - let formData = new FormData(); + const file = item.getAsFile(); + const formData = new FormData(); - if (file) { - formData.append('file', file); + if (!file || !file.type) { + continue; } - let dropFileName = file ? file.name : "-empty-"; - let prom = fetch(upload, { - method: 'POST', - body: formData - }).then(async (res: Response) => { - (await res.json()).map(action((file: any) => { - let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; - let pathname = Utils.prepend(file.path); + 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 => { - doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); - doc && this.props.addDocument(doc); + if (doc) { + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + nativeWidth && (proto["data-nativeWidth"] = nativeWidth); + nativeHeight && (proto["data-nativeHeight"] = nativeHeight); + contentSize && (proto.contentSize = contentSize); + this.props?.addDocument(doc); + } }); })); - }); - promises.push(prom); + })); } } - if (text) { - this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 })); - return; - } if (promises.length) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); } else { + if (text && !text.includes("https://")) { + this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); + } batch.end(); } } diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss new file mode 100644 index 000000000..a5ce73a92 --- /dev/null +++ b/src/client/views/collections/CollectionTimeView.scss @@ -0,0 +1,126 @@ +.collectionTimeView, .collectionTimeView-pivot { + display: flex; + flex-direction: row; + position: absolute; + height: 100%; + width: 100%; + overflow: hidden; + .collectionTimeView-backBtn { + background: green; + display: inline; + margin-right: 20px; + } + .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; + + .collectionTimeView-flyout-item { + background-color: lightgray; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + } + } + + .pivotKeyEntry { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; + pointer-events: all; + padding: 5px; + border: 1px solid black; + } + + .collectionTimeView-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + + .collectionTimeView-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: center; + + .collectionTimeView-button { + align-items: center; + display: flex; + width: 100%; + height: 100%; + + .collectionTimeView-span { + margin: auto; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + text-align: center; + } + } + + .collectionTimeView-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } + } + + .collectionTimeView-innards { + display: inline-block; + width: calc(100% - 200px); + height: 100%; + } + + .collectionTimeView-dragger { + background-color: lightgray; + height: 40px; + width: 20px; + position: absolute; + border-radius: 10px; + top: 55%; + border: 1px black solid; + z-index: 2; + left: -10px; + } +} +.collectionTimeView-pivot { + .collectionFreeform-customText { + text-align: center; + } +}
\ 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..4983acbc2 --- /dev/null +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -0,0 +1,331 @@ +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, trace, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Set } from "typescript-collections"; +import { Doc, DocListCast, Field } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +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 { Scripting } from "../../util/Scripting"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { EditableView } from "../EditableView"; +import { anchorPoints, Flyout } from "../TemplateMenu"; +import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTimeView.scss"; +import React = require("react"); +import { CollectionTreeView } from "./CollectionTreeView"; +import { ObjectField } from "../../../new_fields/ObjectField"; + +@observer +export class CollectionTimeView extends CollectionSubView(doc => doc) { + _changing = false; + @observable _layoutEngine = "pivot"; + + componentDidMount() { + 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_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 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 = 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))); + keySet.toArray().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(); + } + 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(); + } + 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}` }}> + {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} 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" style={{ width: 50, height: 20, background: "green" }} + onClick={action(() => { + let pfilterIndex = NumCast(this.props.Document._pfilterIndex); + if (pfilterIndex > 0) { + this.props.Document._docFilter = ObjectField.MakeCopy(this.props.Document["_pfilter" + --pfilterIndex] as ObjectField); + this.props.Document._pfilterIndex = pfilterIndex; + } else { + this.props.Document._docFilter = 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 pfilterIndex = NumCast(pivotDoc._pfilterIndex); + pivotDoc["_pfilter" + pfilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilter as ObjectField); + pivotDoc._pfilterIndex = ++pfilterIndex; + runInAction(() => { + pivotDoc._docFilter = 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 7d0c900a6..2fa6813d7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -7,15 +7,17 @@ border-radius: inherit; box-sizing: border-box; height: 100%; - width:100%; + width: 100%; position: relative; - top:0; + top: 0; padding-left: 10px; padding-right: 10px; background: $light-color-secondary; font-size: 13px; overflow: auto; + user-select: none; cursor: default; + touch-action: pan-y; ul { list-style: none; @@ -114,6 +116,10 @@ .treeViewItem-header { border: transparent 1px solid; display: flex; + + .editableView-container-editing-oneLine { + min-width: 15px; + } } .treeViewItem-header-above { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 8b993820b..001064b30 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,19 +1,22 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc'; +import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { emptyFunction, Utils, returnFalse } from '../../../Utils'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { emptyFunction, emptyPath, returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { makeTemplate } from '../../util/DropConverter'; +import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -21,38 +24,46 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import { ImageBox } from '../nodes/ImageBox'; import { KeyValueBox } from '../nodes/KeyValueBox'; +import { ScriptBox } from '../ScriptBox'; import { Templates } from '../Templates'; -import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { CollectionViewType } from './CollectionView'; +import { RichTextField } from '../../../new_fields/RichTextField'; +import { ObjectField } from '../../../new_fields/ObjectField'; export interface TreeViewProps { document: Doc; dataDoc?: Doc; + libraryPath: Doc[] | undefined; containingCollection: Doc; + prevSibling?: Doc; renderDepth: number; deleteDoc: (doc: Doc) => boolean; - ruleProvider: Doc | undefined; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; + ChromeHeight: undefined | (() => number); addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; indentDocument?: () => void; + outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; outerXf: () => { translateX: number, translateY: number }; - treeViewId: string; + treeViewId: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; - showHeaderFields: () => boolean; + hideHeaderFields: () => boolean; preventTreeViewOpen: boolean; renderedIds: string[]; + onCheckedClick?: ScriptField; } library.add(faTrashAlt); @@ -81,19 +92,22 @@ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); + + get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive + 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 = c; } - @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; } + 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; } @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; } @computed get fieldKey() { - let splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\""); - return splits.length > 1 ? splits[1].split("\"")[0] : "data"; + const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); + return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { - let layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; + const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) || (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || Cast(this.props.document[field], listSpec(Doc))) as Doc[]; @@ -109,14 +123,14 @@ class TreeView extends React.Component<TreeViewProps> { return this.props.dataDoc; } @computed get boundsOfCollectionDocument() { - return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 ? undefined : + return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 || !DocListCast(this.props.document[this.fieldKey]).length ? undefined : Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"); + @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath); @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete(); - @undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => { + @undoBatch move = (doc: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => { return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } @undoBatch @action remove = (document: Document, key: string) => { @@ -125,7 +139,7 @@ class TreeView extends React.Component<TreeViewProps> { protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer && this._treedropDisposer(); - ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } })); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this))); } onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); @@ -143,11 +157,10 @@ class TreeView extends React.Component<TreeViewProps> { } onDragMove = (e: PointerEvent): void => { Doc.UnBrushDoc(this.dataDoc); - let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75; + const pt = [e.clientX, e.clientY]; + const rect = this._header!.current!.getBoundingClientRect(); + const before = pt[1] < rect.top + rect.height / 2; + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length); this._header!.current!.className = "treeViewItem-header"; if (inside) this._header!.current!.className += " treeViewItem-header-inside"; else if (before) this._header!.current!.className += " treeViewItem-header-above"; @@ -157,22 +170,30 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} - display={"inline"} + display={"inline-block"} editing={this.dataDoc[Id] === TreeView.loadId} contents={StrCast(this.props.document[key])} - height={36} + height={12} fontStyle={style} fontSize={12} GetValue={() => StrCast(this.props.document[key])} SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; - if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined; + const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; return this.props.addDocument(doc); })} - OnTab={() => { TreeView.loadId = ""; this.props.indentDocument && this.props.indentDocument(); }} + OnTab={undoBatch((shift?: boolean) => { + TreeView.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 = ""; + }, 0); + })} />) onWorkspaceContextMenu = (e: React.MouseEvent): void => { @@ -181,18 +202,17 @@ class TreeView extends React.Component<TreeViewProps> { 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"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"), icon: "caret-square-right" }); + 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" }); 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.dataDoc)), icon: "camera" }); + ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.props.document)), icon: "camera" }); } ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } else { - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" }); 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: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); @@ -202,78 +222,78 @@ class TreeView extends React.Component<TreeViewProps> { @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { - let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - let rect = this._header!.current!.getBoundingClientRect(); - let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75 || (!before && this.treeViewOpen); - if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc = de.data.linkSourceDocument; - let destDoc = this.props.document; + const pt = [de.x, de.y]; + const rect = this._header!.current!.getBoundingClientRect(); + const before = pt[1] < rect.top + rect.height / 2; + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length); + if (de.complete.linkDragData) { + const sourceDoc = de.complete.linkDragData.linkSourceDocument; + const destDoc = this.props.document; DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }); e.stopPropagation(); } - if (de.data instanceof DragManager.DocumentDragData) { + if (de.complete.docDragData) { e.stopPropagation(); - if (de.data.draggedDocuments[0] === this.props.document) return true; + if (de.complete.docDragData.draggedDocuments[0] === this.props.document) return true; let addDoc = (doc: Doc) => this.props.addDocument(doc, undefined, before); if (inside) { addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); } - let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments); - return (de.data.dropAction || de.data.userDropAction) ? - de.data.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) - : de.data.moveDocument ? - movedDocs.reduce((added, d) => de.data.moveDocument(d, undefined, addDoc) || added, false) - : de.data.droppedDocuments.reduce((added, d) => addDoc(d), false); + const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); + return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? + de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) + : de.complete.docDragData.moveDocument ? + movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) + : de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d), false); } return false; } docTransform = () => { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); - let outerXf = this.props.outerXf(); - let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); + const outerXf = this.props.outerXf(); + const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)); return finalXf; } docWidth = () => { - let layoutDoc = Doc.Layout(this.props.document); - let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth); + const layoutDoc = Doc.Layout(this.props.document); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { - let layoutDoc = Doc.Layout(this.props.document); - let bounds = this.boundsOfCollectionDocument; + const layoutDoc = Doc.Layout(this.props.document); + const bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, - NumCast(this.props.containingCollection.height)))) : - NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50; + return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.containingCollection._height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; })()); } - expandedField = (doc: Doc) => { - let ids: { [key: string]: string } = {}; + @computed get expandedField() { + const ids: { [key: string]: string } = {}; + const doc = this.props.document; doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); - let rows: JSX.Element[] = []; - for (let key of Object.keys(ids).slice().sort()) { - let contents = doc[key]; + const rows: JSX.Element[] = []; + for (const key of Object.keys(ids).slice().sort()) { + const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; - if (contents instanceof Doc || Cast(contents, listSpec(Doc))) { - let remDoc = (doc: Doc) => this.remove(doc, key); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); + if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { + const remDoc = (doc: Doc) => this.remove(doc, key); + 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, addDoc, remDoc, this.move, + 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.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, doc[Id]]); + 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); } else { contentElement = <EditableView key="editableView" @@ -281,7 +301,7 @@ class TreeView extends React.Component<TreeViewProps> { height={13} fontSize={12} GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value)} />; + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; } rows.push(<div style={{ display: "flex" }} key={key}> <span style={{ fontWeight: "bold" }}>{key + ":"}</span> @@ -289,38 +309,47 @@ class TreeView extends React.Component<TreeViewProps> { {contentElement} </div>); } + rows.push(<div style={{ display: "flex" }} key={"newKeyValue"}> + <EditableView + key="editableView" + contents={"+key:value"} + height={13} + fontSize={12} + GetValue={() => ""} + SetValue={(value: string) => { + value.indexOf(":") !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(":")), value.substring(value.indexOf(":") + 1, value.length), true); + return true; + }} /> + </div>); return rows; } - noOverlays = (doc: Doc) => ({ title: "", caption: "" }); - @computed get renderContent() { const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { - let remDoc = (doc: Doc) => this.remove(doc, expandKey); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); - let docs = expandKey === "links" ? this.childLinks : this.childDocs; + const remDoc = (doc: Doc) => this.remove(doc, expandKey); + const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); + const docs = expandKey === "links" ? this.childLinks : this.childDocs; return <ul key={expandKey + "more"}> {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), - this.templateDataDoc, expandKey, addDoc, remDoc, this.move, + 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.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, this.props.document[Id]])} + 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)} </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}> - {this.expandedField(this.props.document)} + {this.expandedField} </div></ul>; } else { - let layoutDoc = Doc.Layout(this.props.document); + const layoutDoc = Doc.Layout(this.props.document); return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}> <ContentFittingDocumentView Document={layoutDoc} DataDocument={this.templateDataDoc} - renderDepth={this.props.renderDepth} - showOverlays={this.noOverlays} - ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} fitToBox={this.boundsOfCollectionDocument !== undefined} PanelWidth={this.docWidth} PanelHeight={this.docHeight} @@ -333,16 +362,32 @@ class TreeView extends React.Component<TreeViewProps> { active={this.props.active} whenActiveChanged={emptyFunction} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} /> + pinToPres={this.props.pinToPres} /> </div>; } } + @action + bulletClick = (e: React.MouseEvent) => { + if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) { + // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; + ScriptCast(this.props.onCheckedClick).script.run({ + this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, + heading: this.props.containingCollection.title, + checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeViewId, + }, console.log); + } else { + this.treeViewOpen = !this.treeViewOpen; + } + e.stopPropagation(); + } + @computed get renderBullet() { - return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> - {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />} + const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}> + {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } /** @@ -350,10 +395,10 @@ class TreeView extends React.Component<TreeViewProps> { */ @computed get renderTitle() { - let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + const reference = React.createRef<HTMLDivElement>(); + const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); - let headerElements = ( + const headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { if (this.treeViewOpen) { @@ -366,26 +411,27 @@ class TreeView extends React.Component<TreeViewProps> { })}> {this.treeViewExpandedView} </span>); - let openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> + const openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> </div>); return <> <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ color: this.props.document.isMinimized ? "red" : "black", - background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0", - fontWeight: this.props.document.search_string ? "bold" : undefined, + background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", + fontWeight: this.props.document.searchMatch ? "bold" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > {this.editableView("title")} </div > - {this.props.showHeaderFields() ? headerElements : (null)} + {this.props.hideHeaderFields() ? (null) : headerElements} {openRight} </>; } render() { + setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> @@ -399,11 +445,13 @@ class TreeView extends React.Component<TreeViewProps> { </div>; } public static GetChildElements( - docs: Doc[], - treeViewId: string, + childDocs: Doc[], + treeViewId: Doc, containingCollection: Doc, dataDoc: Doc | undefined, key: string, + parentCollectionDoc: Doc | undefined, + parentPrevSibling: Doc | undefined, add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, @@ -414,29 +462,47 @@ class TreeView extends React.Component<TreeViewProps> { outerXf: () => { translateX: number, translateY: number }, active: (outsideReaction?: boolean) => boolean, panelWidth: () => number, + ChromeHeight: undefined | (() => number), renderDepth: number, - showHeaderFields: () => boolean, + hideHeaderFields: () => boolean, preventTreeViewOpen: boolean, - renderedIds: string[] + renderedIds: string[], + libraryPath: Doc[] | undefined, + onCheckedClick: ScriptField | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { - docs = docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); + childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); } - let ascending = Cast(containingCollection.sortAscending, "boolean", null); + const docs = childDocs.slice(); + const dataExtension = containingCollection[key + "_ext"] as Doc; + const ascending = dataExtension && BoolCast(dataExtension.sortAscending, null); if (ascending !== undefined) { - docs.sort(function (a, b): 1 | -1 { - let descA = ascending ? b : a; - let descB = ascending ? a : b; - let first = descA.title; - let second = descB.title; + + const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { + const reN = /[0-9]*$/; + const aA = a.replace(reN, ""); // get rid of trailing numbers + const bA = b.replace(reN, ""); + if (aA === bA) { // if header string matches, then compare numbers numerically + const aN = parseInt(a.match(reN)![0], 10); + const bN = parseInt(b.match(reN)![0], 10); + return aN === bN ? 0 : aN > bN ? 1 : -1; + } else { + return aA > bA ? 1 : -1; + } + }; + docs.sort(function (a, b): 0 | 1 | -1 { + const descA = ascending ? b : a; + const descB = ascending ? a : b; + const first = descA.title; + const second = descB.title; // TODO find better way to sort how to sort.................. if (typeof first === 'number' && typeof second === 'number') { return (first - second) > 0 ? 1 : -1; } if (typeof first === 'string' && typeof second === 'string') { - return first > second ? 1 : -1; + return sortAlphaNum(first, second); } if (typeof first === 'boolean' && typeof second === 'boolean') { // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load @@ -448,17 +514,17 @@ class TreeView extends React.Component<TreeViewProps> { }); } - let rowWidth = () => panelWidth() - 20; + const rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { - const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); if (!pair.layout || pair.data instanceof Promise) { return (null); } - let indent = i === 0 ? undefined : () => { - if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) { - let fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1]; - let fieldKey = fieldKeysub.split("\"")[1]; + const indent = i === 0 ? undefined : () => { + if (StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(docs[i - 1].layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) { Doc.AddDocToList(docs[i - 1], fieldKey, child); docs[i - 1].treeViewOpen = true; @@ -466,27 +532,40 @@ class TreeView extends React.Component<TreeViewProps> { } } }; - let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + const outdent = !parentCollectionDoc ? undefined : () => { + if (StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; + Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); + parentCollectionDoc.treeViewOpen = true; + remove(child); + } + }; + const addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); }; const childLayout = Doc.Layout(pair.layout); - let rowHeight = () => { - let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0); + const rowHeight = () => { + const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return !(child instanceof Doc) ? (null) : <TreeView document={pair.layout} dataDoc={pair.data} + libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined} containingCollection={containingCollection} + prevSibling={docs[i]} treeViewId={treeViewId} - ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc} key={child[Id]} indentDocument={indent} + outdentDocument={outdent} + onCheckedClick={onCheckedClick} renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} panelWidth={rowWidth} panelHeight={rowHeight} + ChromeHeight={ChromeHeight} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} @@ -495,7 +574,7 @@ class TreeView extends React.Component<TreeViewProps> { outerXf={outerXf} parentKey={key} active={active} - showHeaderFields={showHeaderFields} + hideHeaderFields={hideHeaderFields} preventTreeViewOpen={preventTreeViewOpen} renderedIds={renderedIds} />; }); @@ -512,7 +591,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer && this.treedropDisposer(); if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this.treedropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); } } @@ -523,7 +602,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document): boolean => { - let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); return true; @@ -544,10 +623,72 @@ export class CollectionTreeView extends CollectionSubView(Document) { e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } else { - let layoutItems: ContextMenuProps[] = []; - layoutItems.push({ description: this.props.Document.preventTreeViewOpen ? "Persist Treeview State" : "Abandon Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" }); + 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.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); + } + d.captions = undefined; + }); + }); + 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": "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 detailView = Docs.Create.StackingDocument([ + CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }), + textDoc, + TextDocument("", { title: "short_description", _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..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ + description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, + "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", treeViewContainer: Doc.name }) + }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); @@ -562,17 +703,17 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; - let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); - let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); + const dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); + const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( - <div id="body" className="collectionTreeView-dropTarget" - style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} + <div className="collectionTreeView-dropTarget" id="body" + style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - <EditableView + {(this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} display={"block"} maxHeight={72} @@ -581,21 +722,54 @@ 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); - let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; - if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined; + const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); - })} /> + })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { - TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, + TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => !this.props.Document.hideHeaderFields, - BoolCast(this.props.Document.preventTreeViewOpen), []) + 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)) } </ul> </div > ); } -}
\ No newline at end of file +} + +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) { + const allCollectionDocs = DocListCast(dataDoc[dataKey]); + const facetValues = Array.from(allCollectionDocs.reduce((set, child) => + set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); + + 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)", + { layoutDoc: Doc.name, facetHeader: "string", facetValue: "string" }, + { layoutDoc, facetHeader, facetValue }) + })); + return new List<Doc>(facetValueDocSet); +}); + +Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { + const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []); + for (let i = 0; i < docFilters.length; i += 3) { + const [header, value, state] = docFilters.slice(i, i + 3); + if (header === facetHeader && value === facetValue) { + return state; + } + } + return undefined; +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index e4187e4d6..1c46081a1 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -9,7 +9,7 @@ border-radius: inherit; width: 100%; height: 100%; - overflow: auto; + overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... } #google-tags { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8387e95df..bdd908807 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,37 +1,44 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; +import Lightbox from 'react-image-lightbox-with-rotate'; +import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app +import { DateField } from '../../../new_fields/DateField'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { StrCast, BoolCast, Cast } from '../../../new_fields/Types'; +import { listSpec } from '../../../new_fields/Schema'; +import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types'; +import { ImageField } from '../../../new_fields/URLField'; +import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { ScriptBox } from '../ScriptBox'; +import { Touchable } from '../Touchable'; import { CollectionDockingView } from "./CollectionDockingView"; import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionCarouselView } from './CollectionCarouselView'; +import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; +import { CollectionStaffView } from './CollectionStaffView'; import { CollectionTreeView } from "./CollectionTreeView"; +import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { ImageUtils } from '../../util/Import & Export/ImageUtils'; -import { CollectionLinearView } from '../CollectionLinearView'; -import { CollectionStaffView } from './CollectionStaffView'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ImageField } from '../../../new_fields/URLField'; -import { DocListCast } from '../../../new_fields/Doc'; -import Lightbox from 'react-image-lightbox-with-rotate'; -import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app +import { CollectionTimeView } from './CollectionTimeView'; +import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; export const COLLECTION_BORDER_WIDTH = 2; -import { DateField } from '../../../new_fields/DateField'; -import { Doc, } from '../../../new_fields/Doc'; -import { listSpec } from '../../../new_fields/Schema'; -import { DocumentManager } from '../../util/DocumentManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import './CollectionView.scss'; -import { FieldViewProps, FieldView } from '../nodes/FieldView'; -import { Touchable } from '../Touchable'; +const path = require('path'); library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); export enum CollectionViewType { @@ -42,9 +49,13 @@ export enum CollectionViewType { Tree, Stacking, Masonry, - Pivot, + Multicolumn, + Multirow, + Time, + Carousel, Linear, - Staff + Staff, + Timeline } export namespace CollectionViewType { @@ -56,8 +67,11 @@ export namespace CollectionViewType { ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], - ["pivot", CollectionViewType.Pivot], - ["linear", CollectionViewType.Linear] + ["multicolumn", CollectionViewType.Multicolumn], + ["multirow", CollectionViewType.Multirow], + ["time", CollectionViewType.Time], + ["carousel", CollectionViewType.Carousel], + ["linear", CollectionViewType.Linear], ]); export const valueOf = (value: string) => stringMapping.get(value.toLowerCase()); @@ -66,7 +80,7 @@ export namespace CollectionViewType { export interface CollectionRenderProps { addDocument: (document: Doc) => boolean; removeDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; } @@ -84,7 +98,7 @@ export class CollectionView extends Touchable<FieldViewProps> { public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } get collectionViewType(): CollectionViewType | undefined { - let viewField = Cast(this.props.Document.viewType, "number"); + const viewField = NumCast(this.props.Document._viewType); if (CollectionView._safeMode) { if (viewField === CollectionViewType.Freeform) { return CollectionViewType.Tree; @@ -93,15 +107,15 @@ export class CollectionView extends Touchable<FieldViewProps> { return CollectionViewType.Freeform; } } - return viewField === undefined ? CollectionViewType.Invalid : viewField; + return viewField; } componentDidMount = () => { - this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), + this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus), () => { // chrome status is one of disabled, collapsed, or visible. this determines initial state from document // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar. - let chromeStatus = this.props.Document.chromeStatus; + const chromeStatus = this.props.Document._chromeStatus; if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { runInAction(() => this._collapsed = true); } @@ -111,7 +125,7 @@ export class CollectionView extends Touchable<FieldViewProps> { componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer(); // bcz: Argh? What's the height of the collection chromes?? - chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0); active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; @@ -119,19 +133,18 @@ export class CollectionView extends Touchable<FieldViewProps> { @action.bound addDocument(doc: Doc): boolean { - let targetDataDoc = Doc.GetProto(this.props.Document); + const targetDataDoc = Doc.GetProto(this.props.DataDoc || this.props.Document); Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); - let extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field - extension && (extension.lastModified = new DateField(new Date(Date.now()))); + targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); Doc.GetProto(doc).lastOpened = new DateField; return true; } @action.bound removeDocument(doc: Doc): boolean { - let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); + const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); - let value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const value = Cast((this.props.DataDoc || this.props.Document)[this.props.fieldKey], listSpec(Doc), []); 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); @@ -148,7 +161,7 @@ export class CollectionView extends Touchable<FieldViewProps> { // otherwise, the document being moved must be able to be removed from its container before // moving it into the target. @action.bound - moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { + moveDocument(doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean { if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } @@ -163,30 +176,33 @@ export class CollectionView extends Touchable<FieldViewProps> { } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - let props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" }; + const props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, 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.Multirow: return (<CollectionMultirowView chromeCollapsed={true} key="rpwview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } + case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); } case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } - case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); } + case CollectionViewType.Time: { return (<CollectionTimeView key="collview" {...props} />); } case CollectionViewType.Freeform: - default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } + default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } } @action private collapse = (value: boolean) => { this._collapsed = value; - this.props.Document.chromeStatus = value ? "collapsed" : "enabled"; + this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - let chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : + const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; return [chrome, this.SubViewHelper(type, renderProps)]; } @@ -194,25 +210,28 @@ export class CollectionView extends Touchable<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - let existingVm = ContextMenu.Instance.findByDescription("View Modes..."); - let subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); + const existingVm = ContextMenu.Instance.findByDescription("View Modes..."); + const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" }); if (CollectionView._safeMode) { - ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } - subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); - subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" }); - subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); + subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" }); + subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" }); + subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); subItems.push({ description: "Stacking (AutoHeight)", event: () => { - this.props.Document.viewType = CollectionViewType.Stacking; - this.props.Document.autoHeight = true; + this.props.Document._viewType = CollectionViewType.Stacking; + this.props.Document._autoHeight = true; }, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" }); - subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); - subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" }); - switch (this.props.Document.viewType) { + subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" }); + subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" }); + subItems.push({ description: "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/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) }); break; @@ -221,28 +240,47 @@ export class CollectionView extends Touchable<FieldViewProps> { subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); - let existing = ContextMenu.Instance.findByDescription("Layout..."); - let layoutItems = existing && "subitems" in existing ? existing.subitems : []; + const existing = ContextMenu.Instance.findByDescription("Layout..."); + const layoutItems = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + if (this.props.Document.childLayout instanceof Doc) { + layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" }); + } + if (this.props.Document.childDetailed instanceof Doc) { + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" }); + } !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); - let more = ContextMenu.Instance.findByDescription("More..."); - let moreItems = more && "subitems" in more ? more.subitems : []; + const more = ContextMenu.Instance.findByDescription("More..."); + const moreItems = more && "subitems" in more ? more.subitems : []; moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } lightbox = (images: string[]) => { + if (!images.length) return (null); + const mainPath = path.extname(images[this._curLightboxImg]); + const nextPath = path.extname(images[(this._curLightboxImg + 1) % images.length]); + const prevPath = path.extname(images[(this._curLightboxImg + images.length - 1) % images.length]); + const main = images[this._curLightboxImg].replace(mainPath, "_o" + mainPath); + const next = images[(this._curLightboxImg + 1) % images.length].replace(nextPath, "_o" + nextPath); + const prev = images[(this._curLightboxImg + images.length - 1) % images.length].replace(prevPath, "_o" + prevPath); return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox" - mainSrc={images[this._curLightboxImg]} - nextSrc={images[(this._curLightboxImg + 1) % images.length]} - prevSrc={images[(this._curLightboxImg + images.length - 1) % images.length]} + mainSrc={main} + nextSrc={next} + prevSrc={prev} onCloseRequest={action(() => this._isLightboxOpen = false)} onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); } render() { + TraceMobx(); const props: CollectionRenderProps = { addDocument: this.addDocument, removeDocument: this.removeDocument, @@ -258,7 +296,12 @@ export class CollectionView extends Touchable<FieldViewProps> { onContextMenu={this.onContextMenu}> {this.showIsTagged()} {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} - {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField) ? Cast(d.data, ImageField)!.url.href : ""))} + {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => + Cast(d.data, ImageField) ? + (Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ? + Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href + : + ""))} </div>); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 7217b6f30..d2dcf96d7 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -9,8 +9,7 @@ background: lightgrey; .collectionViewChrome { - display: grid; - grid-template-columns: 1fr auto; + display: flex; padding-bottom: 10px; border-bottom: .5px solid rgb(180, 180, 180); overflow: hidden; @@ -20,7 +19,7 @@ .collectionViewBaseChrome-viewPicker { font-size: 75%; - text-transform: uppercase; + //text-transform: uppercase; letter-spacing: 2px; background: rgb(238, 238, 238); color: grey; @@ -45,6 +44,25 @@ padding: 12px 10px 11px 10px; margin-left: 10px; } + .collectionViewBaseChrome-cmdPicker { + margin-left: 3px; + margin-right: 0px; + background: rgb(238, 238, 238); + border: none; + color: grey; + } + .commandEntry-outerDiv { + pointer-events: all; + background-color: gray; + display: flex; + flex-direction: row; + .commandEntry-drop { + color:white; + width:25px; + margin-top: auto; + margin-bottom: auto; + } + } .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; @@ -65,6 +83,18 @@ .collectionViewBaseChrome-viewSpecs { margin-left: 10px; display: grid; + + .collectionViewBaseChrome-filterIcon { + position: relative; + display: flex; + margin: auto; + background: gray; + color: white; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + } .collectionViewBaseChrome-viewSpecsInput { padding: 12px 10px 11px 10px; @@ -252,7 +282,6 @@ .commandEntry-outerDiv { display: flex; flex-direction: column; - width: 165px; height: 40px; } .commandEntry-inputArea { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index fb87996c6..4933c6077 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -13,7 +13,6 @@ import { DragManager } from "../../util/DragManager"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; -import { DocLike } from "../MetadataEntryMenu"; import { CollectionViewType } from "./CollectionView"; import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; @@ -33,38 +32,46 @@ interface Filter { contains: boolean; } -let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); +const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { - title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"], + title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; + _narrativeCommand = { + title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"], + initialize: emptyFunction, + immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) + }; _contentCommand = { - // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... - title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], + title: "=> content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d))) }; _viewCommand = { - title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], - immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; }, - initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; } + title: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], + initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document._panX; button.restoredPanY = this.props.CollectionView.props.Document._panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }, + immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; }, }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; + _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; + _schema_commands = [this._templateCommand, this._narrativeCommand]; _tree_commands = []; private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Tree: return this._tree_commands; + case CollectionViewType.Schema: return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; + case CollectionViewType.Time: return this._freeform_commands; + case CollectionViewType.Carousel: return this._freeform_commands; } return []; } @@ -80,11 +87,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } getFilters = (script: string) => { - let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; - let arr: any[] = re.exec(script); - let toReturn: Filter[] = []; + const re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; + const arr: any[] = re.exec(script); + const toReturn: Filter[] = []; if (arr !== null) { - let filter: Filter = { + const filter: Filter = { key: arr[2], value: arr[3], contains: (arr[1] === "!") ? false : true, @@ -120,14 +127,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro let fields: Filter[] = []; if (this.filterValue) { - let string = this.filterValue.script.originalScript; + const string = this.filterValue.script.originalScript; fields = this.getFilters(string); } runInAction(() => { this.addKeyRestrictions(fields); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - let chromeStatus = this.props.CollectionView.props.Document.chromeStatus; + const chromeStatus = this.props.CollectionView.props.Document._chromeStatus; if (chromeStatus) { if (chromeStatus === "disabled") { throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); @@ -144,24 +151,35 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @undoBatch viewChanged = (e: React.ChangeEvent) => { //@ts-ignore - this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value); + this.props.CollectionView.props.Document._viewType = parseInt(e.target.selectedOptions[0].value); + } + + commandChanged = (e: React.ChangeEvent) => { + //@ts-ignore + runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } @action openViewSpecs = (e: React.SyntheticEvent) => { - this._viewSpecsOpen = true; + if (this._viewSpecsOpen) this.closeViewSpecs(); + else { + this._viewSpecsOpen = true; - //@ts-ignore - if (!e.target.classList[0].startsWith("qs")) { - this.closeDatePicker(); - } + //@ts-ignore + if (!e.target?.classList[0]?.startsWith("qs")) { + this.closeDatePicker(); + } - e.stopPropagation(); - document.removeEventListener("pointerdown", this.closeViewSpecs); - document.addEventListener("pointerdown", this.closeViewSpecs); + e.stopPropagation(); + document.removeEventListener("pointerdown", this.closeViewSpecs); + document.addEventListener("pointerdown", this.closeViewSpecs); + } } - @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); }; + @action closeViewSpecs = () => { + this._viewSpecsOpen = false; + document.removeEventListener("pointerdown", this.closeViewSpecs); + }; @action openDatePicker = (e: React.PointerEvent) => { @@ -183,7 +201,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action addKeyRestriction = (e: React.MouseEvent) => { - let index = this._keyRestrictions.length; + const index = this._keyRestrictions.length; this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]); this.openViewSpecs(e); @@ -192,26 +210,27 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action.bound applyFilter = (e: React.MouseEvent) => { this.openViewSpecs(e); - let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; - let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; - let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; - let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; - let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; + + const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; + const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; + const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; + const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; + const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; let dateRestrictionScript = ""; if (this._dateValue instanceof Date) { - let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); - let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); + const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); + const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; } else { - let createdDate = new Date(this._dateValue); + const createdDate = new Date(this._dateValue); if (!isNaN(createdDate.getTime())) { - let lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); - let upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); + const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); + const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; } } - let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? + const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : "true"; @@ -230,9 +249,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action toggleCollapse = () => { - this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled"; + this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { - this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled"); + this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); } } @@ -250,32 +269,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>) => { - let 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 }); @@ -287,15 +280,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer && this.dropDisposer(); if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); } } @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.data instanceof DragManager.DocumentDragData && de.data.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments)); + if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); e.stopPropagation(); } return true; @@ -355,7 +348,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro dragPointerMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y]; + const [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y]; if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, @@ -371,7 +364,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } render() { - let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; + const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; return ( <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}> <div className="collectionViewChrome"> @@ -390,23 +383,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro className="collectionViewBaseChrome-viewPicker" onPointerDown={stopPropagation} onChange={this.viewChanged} - value={NumCast(this.props.CollectionView.props.Document.viewType)}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option> + value={NumCast(this.props.CollectionView.props.Document._viewType)}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">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" style={{ display: collapsed ? "none" : "grid" }}> - <input className="collectionViewBaseChrome-viewSpecsInput" - placeholder="FILTER" - value={this.filterValue ? this.filterValue.script.originalScript === "return true" ? "" : this.filterValue.script.originalScript : ""} - onChange={(e) => { }} - onPointerDown={this.openViewSpecs} - id="viewSpecsInput" /> - {this.getPivotInput()} + <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> + <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} > + <FontAwesomeIcon icon="filter" size="2x" /> + </div> <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} style={{ @@ -447,17 +438,20 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro </div> </div> <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > - <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-inputArea" onPointerDown={this.autoSuggestDown} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={this.suggestions} - alwaysRenderSuggestions={true} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={this.onSuggestionFetch} - onSuggestionsClearRequested={this.onSuggestionClear} - ref={this._autosuggestRef} /> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> </div> + <select + className="collectionViewBaseChrome-cmdPicker" + onPointerDown={stopPropagation} + onChange={this.commandChanged} + value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option> + {this._buttonizableCommands.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> </div> </div> </div> @@ -478,7 +472,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView getKeySuggestions = async (value: string): Promise<string[]> => { value = value.toLowerCase(); - let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); + const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); } else { @@ -569,44 +563,35 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh @undoBatch togglePreview = () => { - let dividerWidth = 4; - let borderWidth = Number(COLLECTION_BORDER_WIDTH); - let panelWidth = this.props.CollectionView.props.PanelWidth(); - let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); - let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; + const dividerWidth = 4; + const borderWidth = Number(COLLECTION_BORDER_WIDTH); + const panelWidth = this.props.CollectionView.props.PanelWidth(); + const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); + const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; } @undoBatch @action toggleTextwrap = async () => { - let textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); + const textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); if (textwrappedRows.length) { this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]); } else { - let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); - let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); } } render() { - let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); - let textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; + const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); + const textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; return ( <div className="collectionSchemaViewChrome-cont"> <div className="collectionSchemaViewChrome-toggle"> - <div className="collectionSchemaViewChrome-label">Wrap Text: </div> - <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}> - <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}> - {textWrapped ? "on" : "off"} - </div> - </div> - </div> - - <div className="collectionSchemaViewChrome-toggle"> <div className="collectionSchemaViewChrome-label">Show Preview: </div> <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> @@ -622,12 +607,19 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh @observer export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { - @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); } + get dataExtension() { + return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "_ext"] as Doc; + } + @computed private get descending() { + return this.dataExtension && Cast(this.dataExtension.sortAscending, "boolean", null); + } @action toggleSort = () => { - if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined; - else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false; - else this.props.CollectionView.props.Document.sortAscending = true; + if (this.dataExtension) { + if (this.dataExtension.sortAscending) this.dataExtension.sortAscending = undefined; + else if (this.dataExtension.sortAscending === undefined) this.dataExtension.sortAscending = false; + else this.dataExtension.sortAscending = true; + } } render() { diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx index e35b7d7d3..f3071b316 100644 --- a/src/client/views/collections/KeyRestrictionRow.tsx +++ b/src/client/views/collections/KeyRestrictionRow.tsx @@ -1,8 +1,6 @@ import * as React from "react"; import { observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; -import { Doc } from "../../../new_fields/Doc"; interface IKeyRestrictionProps { contains: boolean; @@ -20,13 +18,13 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr render() { if (this._key && this._value) { let parsedValue: string | number = `"${this._value}"`; - let parsed = parseInt(this._value); + const parsed = parseInt(this._value); let type = "string"; if (!isNaN(parsed)) { parsedValue = parsed; type = "number"; } - let scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) || + const scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) || ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))))`; // let doc = new Doc(); // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello")); diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index aa25a900c..a266861bd 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -1,14 +1,25 @@ -.PDS-flyout { - position: absolute; +.parentDocumentSelector-linkFlyout { + div { + overflow: visible !important; + } + .metadataEntry-outerDiv { + overflow: hidden !important; + pointer-events: all; + } +} +.parentDocumentSelector-flyout { + position: relative; z-index: 9999; background-color: #eeeeee; box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - min-width: 150px; color: black; - top: 12px; padding: 10px; border-radius: 3px; + display: inline-block; + height: 100%; + width: 100%; + border-radius: 3px; hr { height: 1px; @@ -21,7 +32,9 @@ } } .parentDocumentSelector-button { - pointer-events: all; + pointer-events: all; + position: relative; + display: inline-block; } .parentDocumentSelector-metadata { pointer-events: auto; @@ -30,6 +43,9 @@ display: inline-block; } .buttonSelector { + div { + overflow: visible !important; + } position: absolute; display: inline-block; padding-left: 5px; diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 4eb9e9d1e..115f8d633 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -6,12 +6,12 @@ import { observable, action, runInAction } from "mobx"; import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast, StrCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { faEdit, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; import { MetadataEntryMenu } from "../MetadataEntryMenu"; import { DocumentView } from "../nodes/DocumentView"; @@ -21,7 +21,13 @@ export const Flyout = higflyout.default; library.add(faEdit); -type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +type SelectorProps = { + Document: Doc, + Views: DocumentView[], + Stack?: any, + addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void +}; + @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -34,7 +40,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { } async fetchDocuments() { - let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); + const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` }); const map: Map<Doc, Doc> = new Map; const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); @@ -49,66 +55,42 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { getOnClick({ col, target }: { col: Doc, target: Doc }) { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / 2; - col.panX = newPanX; - col.panY = newPanY; + if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(target.x) + NumCast(target._width) / 2; + const newPanY = NumCast(target.y) + NumCast(target._height) / 2; + col._panX = newPanX; + col._panY = newPanY; } this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } - get metadataMenu() { - return <div className="parentDocumentSelector-metadata"> - <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => this.props.Views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} - <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> - </Flyout> - </div>; - } render() { return <div > - <div key="metadata">Metadata: {this.metadataMenu}</div> <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} </div>; } } @observer export class ParentDocSelector extends React.Component<SelectorProps> { - @observable hover = false; - - @action - onMouseLeave = () => { - this.hover = false; - } - - @action - onMouseEnter = () => { - this.hover = true; - } - render() { - let flyout; - if (this.hover) { - flyout = ( - <div className="PDS-flyout" title=" "> - <SelectorContextMenu {...this.props} /> - </div> - ); - } - return ( - <span className="parentDocumentSelector-button" style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave}> - <p>^</p> - {flyout} - </span> + const flyout = ( + <div className="parentDocumentSelector-flyout" style={{}} 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}> + <span className="parentDocumentSelector-button" > + <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> + </span> + </Flyout> + </div>; } } @@ -117,32 +99,31 @@ export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any @observable hover = false; @action - onMouseLeave = () => { - this.hover = false; + onPointerDown = (e: React.PointerEvent) => { + this.hover = !this.hover; + e.stopPropagation(); } - - @action - onMouseEnter = () => { - this.hover = true; + customStylesheet(styles: any) { + return { + ...styles, + panel: { + ...styles.panel, + minWidth: "100px" + }, + }; } render() { - let flyout; - if (this.hover) { - let view = DocumentManager.Instance.getDocumentView(this.props.Document); - flyout = !view ? (null) : ( - <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}> - <DocumentButtonBar views={[view]} stack={this.props.Stack} /> - </div> - ); - } - return ( - <span className="buttonSelector" - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave}> - {this.hover ? (null) : <FontAwesomeIcon icon={faEdit} size={"sm"} />} - {flyout} - </span> + const view = DocumentManager.Instance.getDocumentView(this.props.Document); + const flyout = ( + <div className="ParentDocumentSelector-flyout" title=" "> + <DocumentButtonBar views={[view]} stack={this.props.Stack} /> + </div> ); + return <span title="Tap for menu" onPointerDown={e => e.stopPropagation()} className="buttonSelector"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} stylesheet={this.customStylesheet}> + <FontAwesomeIcon icon={faEdit} size={"sm"} /> + </Flyout> + </span>; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index e1d23ddcb..baf09fe5b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -4,113 +4,359 @@ 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 } from "../../../../new_fields/FieldSymbols"; - -interface PivotData { - type: string; - text: string; - x: number; - y: number; - width: number; - height: number; - fontSize: number; -} +import { Id, ToString } from "../../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../../new_fields/ObjectField"; +import { RefField } from "../../../../new_fields/RefField"; export interface ViewDefBounds { + type: string; + text?: string; x: number; y: number; z?: number; - width: number; - height: number; + zIndex?: number; + width?: number; + height?: number; transition?: string; + fontSize?: number; + highlight?: boolean; + color?: string; + payload: any; +} + +export interface PoolData { + x?: number, + y?: number, + z?: 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 (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 + var canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas")); + var context = canvas.getContext("2d"); + context.font = font; + var 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 }[], 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>(); - for (const doc of childDocs) { - const val = doc[StrCast(pivotDoc.pivotField, "title")]; + 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); + let 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); - const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); const docMap = new Map<Doc, ViewDefBounds>(); - const groupNames: PivotData[] = []; + 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: String(key), + text, x, - y: pivotAxisWidth + 50, + y: pivotAxisWidth, width: pivotAxisWidth * expander * numCols, - height: 100, - fontSize: NumCast(pivotDoc.pivotFontSize, 10) + height: max_text, + fontSize, + payload: val }); - for (const doc of val) { - let layoutDoc = Doc.Layout(doc); + 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; + let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; if (hgt > pivotAxisWidth) { hgt = pivotAxisWidth; - wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; } docMap.set(doc, { - x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2, - 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) { - xCount = (pivotAxisWidth - wid) / 2; + xCount = 0; y += pivotAxisWidth * expander; } } x += pivotAxisWidth * (numCols * expander + gap); }); - childPairs.map(pair => { - let 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; - let 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), 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 = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTimeSpan && (curTime - curTimeSpan); + const maxTimeReq = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTimeSpan && (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 = Number.MAX_VALUE; + let maxTime = -Number.MAX_VALUE; + 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: prevKey.toString(), x: x, y: 0, height: fontHeight, fontSize, payload: undefined }); + } + if (!sortedKeys.length && curTime !== undefined) { + groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined }); + } + + const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5); + let stacking: number[] = []; + let zind = 0; + sortedKeys.forEach(key => { + if (curTime !== undefined && curTime > prevKey && curTime <= key) { + groupNames.push({ type: "text", text: curTime.toString(), 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: key.toString(), 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: curTime.toString(), 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: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); + } + + const divider = { type: "div", color: "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, 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[]) { + + 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 { + elements: 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: Math.max(fontHeight, (gname.height || 0) * scale), + fontSize: gname.fontSize, + payload: gname.payload + })))) + }; } export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { return () => { - let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { + const addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox const scriptField = Cast(doc[key], ScriptField); - let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript} + const scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript} // tslint:disable-next-line: no-unnecessary-callback-wrapper onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below onSave={(text, onError) => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 73b45edc6..f04b79ea4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -7,7 +7,8 @@ import React = require("react"); import v5 = require("uuid/v5"); import { DocumentType } from "../../../documents/DocumentTypes"; import { observable, action, reaction, IReactionDisposer } from "mobx"; -import { StrCast, Cast } from "../../../../new_fields/Types"; +import { StrCast } from "../../../../new_fields/Types"; +import { Id } from "../../../../new_fields/FieldSymbols"; export interface CollectionFreeFormLinkViewProps { A: DocumentView; @@ -17,36 +18,61 @@ export interface CollectionFreeFormLinkViewProps { @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { - @observable _opacity: number = 1; - @observable _update: number = 0; + @observable _opacity: number = 0; _anchorDisposer: IReactionDisposer | undefined; @action componentDidMount() { - setTimeout(action(() => this._opacity = 0.05), 750); - this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform()], - () => { - let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - let adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); - let bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); - let a = adiv.getBoundingClientRect(); - let b = bdiv.getBoundingClientRect(); - let abounds = adiv.parentElement!.getBoundingClientRect(); - let bbounds = bdiv.parentElement!.getBoundingClientRect(); - let apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height, + this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)], + action(() => { + setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() + setTimeout(action(() => this._opacity = 0.05), 750); // this will unhighlight the link line. + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); + const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); + const a = adiv.getBoundingClientRect(); + const b = bdiv.getBoundingClientRect(); + const abounds = adiv.parentElement!.getBoundingClientRect(); + const bbounds = bdiv.parentElement!.getBoundingClientRect(); + const apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height, bbounds.left, bbounds.top, bbounds.width, bbounds.height, a.left + a.width / 2, a.top + a.height / 2); - let bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height, + const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height, abounds.left, abounds.top, abounds.width, abounds.height, apt.point.x, apt.point.y); - let afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; - let bfield = afield === "anchor1" ? "anchor2" : "anchor1"; - this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; - this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; - this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; - this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; - this._update++; - } + const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; + const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; + + // really hacky stuff to make the DocuLinkBox display where we want it to: + // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // otherwise, we just use the computed nearest point on the document boundary to the target Document + const targetAhyperlink = window.document.getElementById((this.props.LinkDocs[0][afield] as Doc)[Id]); + const targetBhyperlink = window.document.getElementById((this.props.LinkDocs[0][bfield] as Doc)[Id]); + if (!targetBhyperlink) { + this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; + this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; + } else { + setTimeout(() => { + (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); + const m = targetBhyperlink.getBoundingClientRect(); + const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; + this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; + }, 0); + } + if (!targetAhyperlink) { + this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; + this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; + } else { + setTimeout(() => { + (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[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); + } + }) , { fireImmediately: true }); } @action @@ -55,22 +81,24 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } render() { - let y = this._update; - let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; - let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); - let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); - let apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); + const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); + const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); - let bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height, + const bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height, a.left, a.top, a.width, a.height, apt.point.x, apt.point.y); - let pt1 = [apt.point.x, apt.point.y]; - let pt2 = [bpt.point.x, bpt.point.y]; - return (<line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ opacity: this._opacity }} - x1={`${pt1[0]}`} y1={`${pt1[1]}`} - x2={`${pt2[0]}`} y2={`${pt2[1]}`} />); + const pt1 = [apt.point.x, apt.point.y]; + 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) : + <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]}`} />; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index e9191c176..044d35eca 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -72,11 +72,11 @@ export class CollectionFreeFormLinksView extends React.Component { } @computed get uniqueConnections() { - let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { + const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { if (!drawnPairs.reduce((found, drawnPair) => { - let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); - let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a); - let match = match1 || match2; + const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); + const match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a); + const match = match1 || match2; if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) { drawnPair.l.push(connection.l); } @@ -91,13 +91,11 @@ export class CollectionFreeFormLinksView extends React.Component { } render() { - return ( - <div className="collectionfreeformlinksview-container"> - <svg className="collectionfreeformlinksview-svgCanvas"> - {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections} - </svg> - {this.props.children} - </div> - ); + return <div className="collectionfreeformlinksview-container"> + <svg className="collectionfreeformlinksview-svgCanvas"> + {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections} + </svg> + {this.props.children} + </div>; } }
\ 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 b8148852d..bb9ae4326 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -13,14 +13,14 @@ import v5 = require("uuid/v5"); export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { protected getCursors(): CursorField[] { - let doc = this.props.Document; + const doc = this.props.Document; - let id = CurrentUserUtils.id; + const id = CurrentUserUtils.id; if (!id) { return []; } - let cursors = Cast(doc.cursors, listSpec(CursorField)); + const cursors = Cast(doc.cursors, listSpec(CursorField)); const now = mobxUtils.now(); // const now = Date.now(); @@ -30,7 +30,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV private crosshairs?: HTMLCanvasElement; drawCrosshairs = (backgroundColor: string) => { if (this.crosshairs) { - let ctx = this.crosshairs.getContext('2d'); + const ctx = this.crosshairs.getContext('2d'); if (ctx) { ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, 20, 20); @@ -62,8 +62,8 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV get sharedCursors() { return this.getCursors().map(c => { - let m = c.data.metadata; - let l = c.data.position; + 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" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 070d4aa65..0b5e44ccb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -22,12 +22,29 @@ .collectionFreeform-customText { position: absolute; text-align: center; + overflow-y: auto; + overflow-x: hidden; } .collectionfreeformview-container { // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. touch-action: none; + .collectionfreeformview-placeholder { + background: gray; + width: 100%; + height: 100%; + display: flex; + align-items: center; + .collectionfreeformview-placeholderSpan { + font-size: 32; + display: flex; + text-align: center; + margin: auto; + background: #80808069; + } + } + .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; @@ -52,6 +69,8 @@ left: 0; width: 100%; height: 100%; + align-items: center; + display: flex; } // selection border...? diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index e2b4716ce..c61dcd21f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,19 +1,19 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, observable, trace, ObservableMap, untracked, reaction, runInAction, IReactionDisposer } from "mobx"; +import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer, trace } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; -import { InkTool } from "../../../../new_fields/InkField"; -import { createSchema, makeInterface } from "../../../../new_fields/Schema"; +import { InkTool, InkField, InkData } from "../../../../new_fields/InkField"; +import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; -import { Docs } from "../../../documents/Documents"; +import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; @@ -26,34 +26,35 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; -import { CreatePolyline } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentViewProps } from "../../nodes/DocumentView"; 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, ViewDefResult, computeTimelineLayout, PoolData, ViewDefBounds } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { computedFn, keepAlive } from "mobx-utils"; +import { computedFn } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; import { Timeline } from "../../animationtimeline/Timeline"; +import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ - panX: "number", - panY: "number", + _panX: "number", + _panY: "number", scale: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", - isRuleProvider: "boolean", fitToBox: "boolean", + xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", @@ -72,23 +73,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _clusterDistance: number = 75; private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; - private _layoutPoolData = new ObservableMap<string, any>(); + private _layoutPoolData = observable.map<string, any>(); - public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; - @observable _timelineRef = React.createRef<Timeline>(); + @observable _timelineRef = React.createRef<Timeline>(); - @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } + @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } - @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } - @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); } + @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); } + @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private easing = () => this.props.Document.panTransformType === "Ease"; - private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0; - private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0; + private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; + private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ? Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : this.Document.scale || 1) @@ -100,18 +101,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); - let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0; - if (heading === 0) { - let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 : - DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0); - heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading); - } - !this.Document.isRuleProvider && (newBox.heading = heading); this.addDocument(newBox); } private addDocument = (newBox: Doc) => { - let added = this.props.addDocument(newBox); + const added = this.props.addDocument(newBox); added && this.bringToFront(newBox); added && this.updateCluster(newBox); return added; @@ -128,54 +121,55 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onDrop = (e: React.DragEvent): Promise<void> => { - var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + const pt = this.getTransform().transformPoint(e.pageX, e.pageY); return super.onDrop(e, { x: pt[0], y: pt[1] }); } @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - let xf = this.getTransform(); - let xfo = this.getTransformOverlay(); - let [xp, yp] = xf.transformPoint(de.x, de.y); - let [xpo, ypo] = xfo.transformPoint(de.x, de.y); + 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 (de.data instanceof DragManager.DocumentDragData) { - if (de.data.droppedDocuments.length) { - let firstDoc = de.data.droppedDocuments[0]; - let z = NumCast(firstDoc.z); - let x = (z ? xpo : xp) - de.data.offset[0]; - let y = (z ? ypo : yp) - de.data.offset[1]; - let dropX = NumCast(firstDoc.x); - let dropY = NumCast(firstDoc.y); - de.data.droppedDocuments.forEach(action((d: Doc) => { - let layoutDoc = Doc.Layout(d); + if (de.complete.docDragData) { + if (de.complete.docDragData.droppedDocuments.length) { + const firstDoc = de.complete.docDragData.droppedDocuments[0]; + const z = NumCast(firstDoc.z); + const x = (z ? xpo : xp) - de.complete.docDragData.offset[0]; + const y = (z ? ypo : yp) - de.complete.docDragData.offset[1]; + const dropX = NumCast(firstDoc.x); + const dropY = NumCast(firstDoc.y); + de.complete.docDragData.droppedDocuments.forEach(action((d: Doc) => { + const layoutDoc = Doc.Layout(d); d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; - if (!NumCast(layoutDoc.width)) { - layoutDoc.width = 300; + if (!NumCast(layoutDoc._width)) { + layoutDoc._width = 300; } - if (!NumCast(layoutDoc.height)) { - let nw = NumCast(layoutDoc.nativeWidth); - let nh = NumCast(layoutDoc.nativeHeight); - layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300; + if (!NumCast(layoutDoc._height)) { + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; } this.bringToFront(d); })); - de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]); + (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); } } - else if (de.data instanceof DragManager.AnnotationDragData) { - if (de.data.dropDocument) { - let dragDoc = de.data.dropDocument; - let x = xp - de.data.offset[0]; - let y = yp - de.data.offset[1]; - let dropX = NumCast(dragDoc.x); - let dropY = NumCast(dragDoc.y); + else if (de.complete.annoDragData) { + if (de.complete.annoDragData.dropDocument) { + const dragDoc = de.complete.annoDragData.dropDocument; + const x = xp - de.complete.annoDragData.offset[0]; + const y = yp - de.complete.annoDragData.offset[1]; + const dropX = NumCast(dragDoc.x); + const dropY = NumCast(dragDoc.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; dragDoc.y = y + NumCast(dragDoc.y) - dropY; - de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation + de.complete.annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation this.bringToFront(dragDoc); } } @@ -185,31 +179,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { pickCluster(probe: number[]) { return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { - let layoutDoc = Doc.Layout(cd); - let cx = NumCast(cd.x) - this._clusterDistance; - let cy = NumCast(cd.y) - this._clusterDistance; - let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance; - let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance; + const layoutDoc = Doc.Layout(cd); + const cx = NumCast(cd.x) - this._clusterDistance; + const cy = NumCast(cd.y) - this._clusterDistance; + const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance; + const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance; return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? NumCast(cd.cluster) : cluster; }, -1); } tryDragCluster(e: PointerEvent | TouchEvent) { - let ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); + const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); if (ptsParent) { - let cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY)); + const cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY)); if (cluster !== -1) { - let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); - let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); - let de = new DragManager.DocumentDragData(eles); + const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); + const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); + const de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; - DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { - handlers: { dragComplete: action(emptyFunction) }, - hideSource: !de.dropAction - }); + DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { hideSource: !de.dropAction }); return true; } } @@ -224,13 +215,48 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } + @action + updateClusterDocs(docs: Doc[]) { + const childLayouts = this.childLayoutPairs.map(pair => pair.layout); + if (this.props.Document.useClusters) { + const docFirst = docs[0]; + docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); + const preferredInd = NumCast(docFirst.cluster); + docs.map(doc => doc.cluster = -1); + docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { + if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + docFirst.cluster = i; + } + }))); + if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + docFirst.cluster = preferredInd; + } + this._clusterSets.map((set, i) => { + if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + docFirst.cluster = i; + } + }); + if (docFirst.cluster === -1) { + docs.map(doc => { + doc.cluster = this._clusterSets.length; + this._clusterSets.push([doc]); + }); + } else { + for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); + docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); + } + childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); + childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString()); + } + } + @undoBatch @action updateCluster(doc: Doc) { - let childLayouts = this.childLayoutPairs.map(pair => pair.layout); + const childLayouts = this.childLayoutPairs.map(pair => pair.layout); if (this.props.Document.useClusters) { this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); - let preferredInd = NumCast(doc.cluster); + const preferredInd = NumCast(doc.cluster); doc.cluster = -1; this._clusterSets.map((set, i) => set.map(member => { if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { @@ -257,15 +283,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getClusterColor = (doc: Doc) => { let clusterColor = ""; - let cluster = NumCast(doc.cluster); + 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 - let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; + const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; clusterColor = colors[cluster % colors.length]; - let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); + const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); // 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)); @@ -274,57 +300,132 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return clusterColor; } - @observable private _points: { x: number, y: number }[] = []; @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + return; + } this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; - if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); + // if physically using a pen or we're in pen or highlighter mode + // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(e.pageX, e.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } + // if not using a pen and in no ink mode if (InkingControl.Instance.selectedTool === InkTool.None) { this._lastX = e.pageX; this._lastY = e.pageY; } + // eraser or scrubber plus anything else mode else { e.stopPropagation(); e.preventDefault(); + } + } + // if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + // document.removeEventListener("pointermove", this.onPointerMove); + // document.removeEventListener("pointerup", this.onPointerUp); + // document.addEventListener("pointermove", this.onPointerMove); + // document.addEventListener("pointerup", this.onPointerUp); + // if (InkingControl.Instance.selectedTool === InkTool.None) { + // this._lastX = e.pageX; + // this._lastY = e.pageY; + // } + // else { + // e.stopPropagation(); + // e.preventDefault(); + + // if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { + // let point = this.getTransform().transformPoint(e.pageX, e.pageY); + // this._points.push({ x: point[0], y: point[1] }); + // } + // } + // } + } - if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - let point = this.getTransform().transformPoint(e.pageX, e.pageY); - this._points.push({ x: point[0], y: point[1] }); + @action + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble) { + // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt = me.changedTouches[0]; + if (pt) { + this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } + if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = pt.pageX; + this._lastY = pt.pageY; + e.preventDefault(); + e.stopPropagation(); + } + else { + e.preventDefault(); + } } } } } - @action - handle1PointerDown = (e: React.TouchEvent) => { - let pt = e.targetTouches.item(0); - if (pt) { - this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + @undoBatch + onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { + switch (ge.gesture) { + case GestureUtils.Gestures.Stroke: + const points = ge.points; + const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + this.addDocument(inkDoc); + e.stopPropagation(); + break; + case GestureUtils.Gestures.Box: + const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); + const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); + const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] }; + const bWidth = bounds.r - bounds.x; + const bHeight = bounds.b - bounds.y; + const sel = this.getActiveDocuments().filter(doc => { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); + if (pass) { + doc.x = l - bounds.x - bWidth / 2; + doc.y = t - bounds.y - bHeight / 2; + } + return pass; + }); + this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); + sel.forEach(d => this.props.removeDocument(d)); + break; + } } @action onPointerUp = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.TOUCH) && this._points.length <= 1) return; - - if (this._points.length > 1) { - let B = this.svgBounds; - let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top })); - let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); - this.addDocument(inkDoc); - this._points = []; - } + if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -332,28 +433,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // I think it makes sense for the marquee menu to go away when panned. -syip2 MarqueeOptionsMenu.Instance.fadeOut(true); - let x = this.Document.panX || 0; - let y = this.Document.panY || 0; - let docs = this.childLayoutPairs.map(pair => pair.layout); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay) { + let x = this.Document._panX || 0; + let y = this.Document._panY || 0; + const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); + const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) { PDFMenu.Instance.fadeOut(true); - let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; - let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; - let ranges = docs.filter(doc => doc).reduce((range, doc) => { - let layoutDoc = Doc.Layout(doc); - let x = NumCast(doc.x); - let xe = x + NumCast(layoutDoc.width); - let y = NumCast(doc.y); - let ye = y + NumCast(layoutDoc.height); + 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).filter(doc => this.childDataProvider(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); 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]]); - let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; - let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, + const cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; + const panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, this.props.PanelHeight() / this.zoomScaling() * cscale); if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; @@ -367,14 +467,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) { + if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { if (this.props.active(true)) { e.stopPropagation(); } return; } + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + return; + } if (!e.cancelBubble) { - if (InkingControl.Instance.selectedTool === InkTool.None) { + const selectedTool = InkingControl.Instance.selectedTool; + if (selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -384,19 +488,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } this.pan(e); } - else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - let point = this.getTransform().transformPoint(e.clientX, e.clientY); - this._points.push({ x: point[0], y: point[1] }); - } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } - handle1PointerMove = (e: TouchEvent) => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // panning a workspace if (!e.cancelBubble) { - let pt = e.targetTouches.item(0); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { @@ -408,77 +509,85 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } this.pan(pt); } - else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - let point = this.getTransform().transformPoint(pt.clientX, pt.clientY); - this._points.push({ x: point[0], y: point[1] }); - } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } - handle2PointersMove = (e: TouchEvent) => { + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // pinch zooming if (!e.cancelBubble) { - let pt1: Touch | null = e.targetTouches.item(0); - let pt2: Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; if (this.prevPoints.size === 2) { - let oldPoint1 = this.prevPoints.get(pt1.identifier); - let oldPoint2 = this.prevPoints.get(pt2.identifier); + const oldPoint1 = this.prevPoints.get(pt1.identifier); + const oldPoint2 = this.prevPoints.get(pt2.identifier); if (oldPoint1 && oldPoint2) { - let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2); + const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2); // if zooming, zoom if (dir !== 0) { - let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2)); - let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2)); - let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2)); + const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2)); + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; // calculate the raw delta value - let rawDelta = (dir * (d1 + d2)); + const rawDelta = (dir * (d1 + d2)); // this floors and ceils the delta value to prevent jitteriness - let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16); - this.zoom(centerX, centerY, delta); + const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8); + this.zoom(centerX, centerY, delta * window.devicePixelRatio); this.prevPoints.set(pt1.identifier, pt1); this.prevPoints.set(pt2.identifier, pt2); } // this is not zooming. derive some form of panning from it. else { // use the centerx and centery as the "new mouse position" - let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this.pan({ clientX: centerX, clientY: centerY }); this._lastX = centerX; this._lastY = centerY; } } } + // e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } - handle2PointersDown = (e: React.TouchEvent) => { - let pt1: React.Touch | null = e.targetTouches.item(0); - let pt2: React.Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; - - let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; - this._lastX = centerX; - this._lastY = centerY; + @action + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble && this.props.active(true)) { + // const pt1: React.Touch | null = e.targetTouches.item(0); + // const pt2: React.Touch | null = e.targetTouches.item(1); + // // if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + if (pt1 && pt2) { + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } + } } cleanUpInteractions = () => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -488,11 +597,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { deltaScale = 1 / this.zoomScaling(); } if (deltaScale < 0) deltaScale = -deltaScale; - let [x, y] = this.getTransform().transformPoint(pointX, pointY); - let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + const [x, y] = this.getTransform().transformPoint(pointX, pointY); + const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.15) { - let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); + const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); } @@ -514,11 +623,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { setPan(panX: number, panY: number, panType: string = "None") { if (!this.Document.lockedTransform || this.Document.inOverlay) { this.Document.panTransformType = panType; - var scale = this.getLocalTransform().inverse().Scale; + const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); - this.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + this.Document._panX = this.isAnnotationOverlay ? newPanX : panX; + this.Document._panY = this.isAnnotationOverlay ? newPanY : panY; } } @@ -539,48 +648,50 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); + // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state - if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) { + if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; if (!init) { - state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY }; + state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY }; HistoryUtil.pushState(state); - } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) { - init.panX = this.Document.panX; - init.panY = this.Document.panY; + } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) { + init.panX = this.Document._panX; + init.panY = this.Document._panY; HistoryUtil.pushState(state); } } SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { - let annotOn = Cast(doc.annotationOn, Doc) as Doc; + const annotOn = Cast(doc.annotationOn, Doc) as Doc; if (!annotOn) { this.props.focus(doc); } else { - let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height); - let offset = annotOn && (contextHgt / 2 * 96 / 72); + const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height); + const offset = annotOn && (contextHgt / 2 * 96 / 72); this.props.Document.scrollY = NumCast(doc.y) - offset; } } else { - let layoutdoc = Doc.Layout(doc); - const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2; + const layoutdoc = Doc.Layout(doc); + const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; + const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2; const newState = HistoryUtil.getState(); newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; - this.setPan(newPanX, newPanY, "Ease"); + if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); willZoom && this.setScaleToZoom(layoutdoc, scale); + Doc.linkFollowHighlight(doc); afterFocus && setTimeout(() => { if (afterFocus && afterFocus()) { - this.Document.panX = savedState.px; - this.Document.panY = savedState.py; + this.Document._panX = savedState.px; + this.Document._panY = savedState.py; this.Document.scale = savedState.s; this.Document.panTransformType = savedState.pt; } @@ -590,7 +701,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { - this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height)); + this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } zoomToScale = (scale: number) => { @@ -599,14 +710,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale = () => this.Document.scale || 1; + @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { ...this.props, DataDoc: childData, Document: childLayout, + LibraryPath: this.libraryPath, layoutKey: undefined, - ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves - onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them + //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them + onClick: this.onChildClickHandler, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], @@ -623,73 +738,143 @@ 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") }; + return { + x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), color: Cast(params.doc.color, "string"), + zIndex: Cast(params.doc.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> { - if (viewDef.type === "text") { + 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 = Cast(viewDef.x, "number"); + const y = Cast(viewDef.y, "number"); + const z = Cast(viewDef.z, "number"); + const highlight = Cast(viewDef.highlight, "boolean"); + const zIndex = Cast(viewDef.zIndex, "number"); + const color = Cast(viewDef.color, "string"); + const width = Cast(viewDef.width, "number", null); + const height = Cast(viewDef.height, "number", null); + 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" 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: `translate(${x}px, ${y}px)` }}> {text} </div>, - bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + bounds: viewDef + }; + } else if (viewDef.type === "div") { + const backgroundColor = Cast(viewDef.color, "string"); + 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, transform: `translate(${x}px, ${y}px)` }} />, + bounds: viewDef }; } } - childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); }.bind(this)); + childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { + if (!doc) { + console.log(doc); + } + return this._layoutPoolData.get(doc[Id]); + }.bind(this)); + + doTimelineLayout(poolData: Map<string, any>) { + return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); + } - doPivotLayout(poolData: ObservableMap<string, any>) { - return computePivotLayout(poolData, this.props.Document, this.childDocs, - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); + doPivotLayout(poolData: Map<string, any>) { + return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs, + this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } - doFreeformLayout(poolData: ObservableMap<string, any>) { - let layoutDocs = this.childLayoutPairs.map(pair => pair.layout); + _cachedPool: Map<string, any> = new Map(); + doFreeformLayout(poolData: Map<string, any>) { + 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; - let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : []; + 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 }; } - 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) }; } - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => + return { newPool, computedElementData: this.doFreeformLayout(newPool) }; + } + + @computed get filterDocs() { + 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 ? this.childDocs.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; + }) : this.childDocs; + return filteredDocs; + } + 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))); + this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair => computedElementData.elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider} - ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} - jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} + dataProvider={this.childDataProvider} + jitterRotation={NumCast(this.props.Document.jitterRotation)} + fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />, bounds: this.childDataProvider(pair.layout) })); @@ -697,12 +882,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } componentDidMount() { - this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; }, - action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)), + super.componentDidMount(); + this._layoutComputeReaction = reaction( + () => (this.doLayoutComputation), + (computation) => this._layoutElements = computation?.elements.slice() || [], { 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; @@ -714,58 +901,78 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { - const docs = DocListCast(this.Document[this.props.fieldKey]); - let startX = this.Document.panX || 0; + const docs = this.childLayoutPairs; + const startX = this.Document._panX || 0; let x = startX; - let y = this.Document.panY || 0; + let y = this.Document._panY || 0; let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); - 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"); } - autoFormat = () => { - this.Document.isRuleProvider = !this.Document.isRuleProvider; - // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading. - this.Document.isRuleProvider && this.childLayoutPairs.map(pair => - // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template) - DocListCast(Doc.Layout(pair.layout).data).map(heading => { - let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); - let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout; - if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) { - Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor; - } - }) - ); - } - - analyzeStrokes = async () => { - // CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], data.inkData); - } + private thumbIdentifier?: number; + + // @action + // handleHandDown = (e: React.TouchEvent) => { + // const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + // const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); + // this.thumbIdentifier = thumb?.identifier; + // const others = fingers.filter(f => f !== thumb); + // const minX = Math.min(...others.map(f => f.clientX)); + // const minY = Math.min(...others.map(f => f.clientY)); + // const t = this.getTransform().transformPoint(minX, minY); + // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY); + + // const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc)); + // if (thumbDoc) { + // this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />; + // } + + // document.removeEventListener("touchmove", this.onTouch); + // document.removeEventListener("touchmove", this.handleHandMove); + // document.addEventListener("touchmove", this.handleHandMove); + // document.removeEventListener("touchend", this.handleHandUp); + // document.addEventListener("touchend", this.handleHandUp); + // } + + // @action + // handleHandMove = (e: TouchEvent) => { + // for (let i = 0; i < e.changedTouches.length; i++) { + // const pt = e.changedTouches.item(i); + // if (pt?.identifier === this.thumbIdentifier) { + // } + // } + // } + + // @action + // handleHandUp = (e: TouchEvent) => { + // this.onTouchEnd(e); + // if (this.prevPoints.size < 3) { + // this._palette = undefined; + // document.removeEventListener("touchend", this.handleHandUp); + // } + // } onContextMenu = (e: React.MouseEvent) => { - let layoutItems: ContextMenuProps[] = []; + const layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) { - layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); - } - this._timelineRef.current!.timelineContextMenu(e.nativeEvent); - layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" }); layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); - layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); + // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); layoutItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => { @@ -774,7 +981,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { input.accept = ".zip"; input.onchange = async _e => { const upload = Utils.prepend("/uploadDoc"); - let formData = new FormData(); + const formData = new FormData(); const file = input.files && input.files[0]; if (file) { formData.append('file', file); @@ -806,7 +1013,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { description: "Add Note ...", subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ description: (i + 1) + ": " + StrCast(note.title), - event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })), + event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })), icon: "eye" })) as ContextMenuProps[], icon: "eye" @@ -816,44 +1023,43 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private childViews = () => { - let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; + const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; return [ ...children, ...this.views, ]; } - @computed get svgBounds() { - let xs = this._points.map(p => p.x); - let ys = this._points.map(p => p.y); - let right = Math.max(...xs); - let left = Math.min(...xs); - let bottom = Math.max(...ys); - let top = Math.min(...ys); - return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top }; - } - - @computed get currentStroke() { - if (this._points.length <= 1) { - return (null); - } - - let B = this.svgBounds; - - return ( - <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}> - {CreatePolyline(this._points, B.left, B.top)} - </svg> - ); - } + // @observable private _palette?: JSX.Element; children = () => { - let eles: JSX.Element[] = []; - this.extensionDoc && (eles.push(...this.childViews())); - this.currentStroke && (eles.push(this.currentStroke)); + 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; } + @computed get placeholder() { + return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> + </div>; + } + @computed get marqueeView() { + return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} + addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> + <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} + easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + {this.children} + </CollectionFreeFormViewPannableContents> + </MarqueeView>; + } + @computed get contentScaling() { + if (this.props.annotationsKey) return 0; + const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; + const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; + return wscale < hscale ? wscale : hscale; + } render() { TraceMobx(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) @@ -863,20 +1069,23 @@ 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 - return !this.extensionDoc ? (null) : - <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, - style={{ height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> - <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} - addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} - easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - {this.children} - </CollectionFreeFormViewPannableContents> - </MarqueeView> - <Timeline ref={this._timelineRef} {...this.props} /> - <CollectionFreeFormOverlayView elements={this.elementFunc} /> - </div>; + // 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} + style={{ + pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + transform: this.contentScaling ? `scale(${this.contentScaling})` : "", + transformOrigin: this.contentScaling ? "left top" : "", + width: this.contentScaling ? `${100 / this.contentScaling}%` : "", + height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + }}> + {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? + this.placeholder : this.marqueeView} + <Timeline ref={this._timelineRef} {...this.props} /> + <CollectionFreeFormOverlayView elements={this.elementFunc} /> + </div>; } } @@ -904,13 +1113,13 @@ interface CollectionFreeFormViewPannableContentsProps { @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ render() { - let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none"); + const freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); - return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> + return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children()} </div>; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 28ddc19d7..71f265484 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -21,22 +21,25 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { } render() { - let buttons = [ + const buttons = [ <button className="antimodeMenu-button" title="Create a Collection" + key="group" onPointerDown={this.createCollection}> <FontAwesomeIcon icon="object-group" size="lg" /> </button>, <button className="antimodeMenu-button" title="Summarize Documents" + key="summarize" onPointerDown={this.summarize}> <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> </button>, <button className="antimodeMenu-button" title="Delete Documents" + key="delete" onPointerDown={this.delete}> <FontAwesomeIcon icon="trash-alt" size="lg" /> </button>, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index d14495626..18d6da0da 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -5,10 +5,9 @@ left:0; width:100%; height:100%; -} -.marqueeView { overflow: hidden; pointer-events: inherit; + border-radius: inherit; } .marqueeView:focus-within { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5ed3fecb5..e16f4011e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../../new_fields/Doc"; -import { InkField, PointData } from "../../../../new_fields/InkField"; +import { InkField } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { listSpec } from "../../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; @@ -15,11 +15,9 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionViewType } from "../CollectionView"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; -import InkSelectDecorations from "../../InkSelectDecorations"; import { SubCollectionViewProps } from "../CollectionSubView"; interface MarqueeViewProps { @@ -31,7 +29,6 @@ interface MarqueeViewProps { removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - extensionDoc: Doc; isAnnotationOverlay?: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -67,26 +64,27 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action onKeyPress = (e: KeyboardEvent) => { //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) { e.preventDefault(); (async () => { - let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + const text: string = await navigator.clipboard.readText(); + const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); for (let i = 0; i < ns.length - 1; i++) { while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || ns[i].endsWith(";\r") || ns[i].endsWith(";") || ns[i].endsWith(".\r") || ns[i].endsWith(".") || ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) { - let sub = ns[i].endsWith("\r") ? 1 : 0; - let br = ns[i + 1].trim() === ""; + const sub = ns[i].endsWith("\r") ? 1 : 0; + const br = ns[i + 1].trim() === ""; ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft()); if (br) break; } } ns.map(line => { - let indent = line.search(/\S|$/); - let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + const indent = line.search(/\S|$/); + const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, title: line }); this.props.addDocument(newBox); y += 40 * this.props.getTransform().Scale; }); @@ -94,19 +92,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else if (e.key === "b" && e.ctrlKey) { e.preventDefault(); navigator.clipboard.readText().then(text => { - let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } }); } else if (!e.ctrlKey) { this.props.addLiveTextDocument( - Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" })); + Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" })); } else if (e.keyCode > 48 && e.keyCode <= 57) { - let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); - let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }); + const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); + const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); text.layout = notes[(e.keyCode - 49) % notes.length]; this.props.addLiveTextDocument(text); } @@ -124,31 +122,31 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque ns.splice(0, 1); } if (ns.length > 0) { - let columns = ns[0].split("\t"); - let docList: Doc[] = []; + const columns = ns[0].split("\t"); + const docList: Doc[] = []; let groupAttr: string | number = ""; - let rowProto = new Doc(); + const rowProto = new Doc(); rowProto.title = rowProto.Id; - rowProto.width = 200; + rowProto._width = 200; rowProto.isPrototype = true; for (let i = 1; i < ns.length - 1; i++) { - let values = ns[i].split("\t"); + const values = ns[i].split("\t"); if (values.length === 1 && columns.length > 1) { groupAttr = values[0]; continue; } - let docDataProto = Doc.MakeDelegate(rowProto); + const docDataProto = Doc.MakeDelegate(rowProto); docDataProto.isPrototype = true; columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); if (groupAttr) { docDataProto._group = groupAttr; } docDataProto.title = i.toString(); - let doc = Doc.MakeDelegate(docDataProto); - doc.width = 200; + const doc = Doc.MakeDelegate(docDataProto); + doc._width = 200; docList.push(doc); } - let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 }); this.props.addDocument(newCol); } @@ -193,13 +191,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onPointerUp = (e: PointerEvent): void => { if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []); if (this._visible) { - let mselect = this.marqueeSelect(); + const mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document); } // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map(); // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : []; - let docs = mselect.length ? mselect : [this.props.Document]; + const docs = mselect.length ? mselect : [this.props.Document]; this.props.selectDocuments(docs, []); } if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { @@ -212,7 +210,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } this.cleanupInteractions(true, this._commandExecuted); - let hideMarquee = () => { + const hideMarquee = () => { this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); document.removeEventListener("pointerdown", hideMarquee); @@ -260,23 +258,23 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @computed get Bounds() { - let left = this._downX < this._lastX ? this._downX : this._lastX; - let top = this._downY < this._lastY ? this._downY : this._lastY; - let topLeft = this.props.getTransform().transformPoint(left, top); - let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + const left = this._downX < this._lastX ? this._downX : this._lastX; + const top = this._downY < this._lastY ? this._downY : this._lastY; + const topLeft = this.props.getTransform().transformPoint(left, top); + const size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } get inkDoc() { - return this.props.extensionDoc; + return this.props.Document; } get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. - return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField); + return Cast(this.props.Document.ink, InkField); } set ink(value: InkField | undefined) { - this.props.extensionDoc && (this.props.extensionDoc.ink = value); + this.props.Document.ink = value; } @action @@ -301,40 +299,23 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[]) => { - let bounds = this.Bounds; - let 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)",]; - let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string")); - if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette); - let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]); - let usedPaletted = new Map<string, number>(); - [...this.props.activeDocuments(), this.props.Document].map(child => { - let 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)"); - let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); - let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; - let inkData = this.ink ? this.ink.inkData : undefined; - let newCollection = Docs.Create.FreeformDocument(selected, { + getCollection = (selected: Doc[], asTemplate: boolean) => { + const bounds = this.Bounds; + // const inkData = this.ink ? this.ink.inkData : undefined; + const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; + const newCollection = creator(selected, { x: bounds.left, y: bounds.top, - panX: 0, - panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - width: bounds.width, - height: bounds.height, + _panX: 0, + _panY: 0, + backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : "white", + defaultBackgroundColor: this.props.isAnnotationOverlay ? "#00000015" : "white", + _width: bounds.width, + _height: bounds.height, + _LODdisable: true, title: "a nested collection", }); - let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); + // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; // this.marqueeInkDelete(inkData); this.hideMarquee(); @@ -343,8 +324,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { - let bounds = this.Bounds; - let selected = this.marqueeSelect(false); + const bounds = this.Bounds; + const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === "c" : true) { selected.map(d => { this.props.removeDocument(d); @@ -354,7 +335,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return d; }); } - let newCollection = this.getCollection(selected); + const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t"); this.props.addDocument(newCollection); this.props.selectDocuments([newCollection], []); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -363,9 +344,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { - let bounds = this.Bounds; - let selected = this.marqueeSelect(false); - let newCollection = this.getCollection(selected); + const bounds = this.Bounds; + const selected = this.marqueeSelect(false); + const newCollection = this.getCollection(selected, false); selected.map(d => { this.props.removeDocument(d); @@ -374,16 +355,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection._chromeStatus = "disabled"; + const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); newCollection.x = bounds.left + bounds.width; Doc.GetProto(newCollection).summaryDoc = summary; Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; + const container = Docs.Create.FreeformDocument([summary, newCollection], { + x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true, + _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-" + }); Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" this.props.addLiveTextDocument(container); } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them @@ -406,12 +388,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "s" || e.key === "S") { + if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - if (e.key === "c") { + if (e.key === "c" || e.key === "t") { this.collection(e); } @@ -462,42 +444,42 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // } marqueeSelect(selectBackgrounds: boolean = true) { - let selRect = this.Bounds; - let selection: Doc[] = []; + const selRect = this.Bounds; + const selection: Doc[] = []; this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => { - let layoutDoc = Doc.Layout(doc); - var x = NumCast(doc.x); - var y = NumCast(doc.y); - var w = NumCast(layoutDoc.width); - var h = NumCast(layoutDoc.height); + const layoutDoc = Doc.Layout(doc); + const x = NumCast(doc.x); + const y = NumCast(doc.y); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } }); if (!selection.length && selectBackgrounds) { this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => { - let layoutDoc = Doc.Layout(doc); - var x = NumCast(doc.x); - var y = NumCast(doc.y); - var w = NumCast(layoutDoc.width); - var h = NumCast(layoutDoc.height); + const layoutDoc = Doc.Layout(doc); + const x = NumCast(doc.x); + const y = NumCast(doc.y); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } }); } if (!selection.length) { - let left = this._downX < this._lastX ? this._downX : this._lastX; - let top = this._downY < this._lastY ? this._downY : this._lastY; - let topLeft = this.props.getContainerTransform().transformPoint(left, top); - let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; + const left = this._downX < this._lastX ? this._downX : this._lastX; + const top = this._downY < this._lastY ? this._downY : this._lastY; + const topLeft = this.props.getContainerTransform().transformPoint(left, top); + const size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + const otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => { - let layoutDoc = Doc.Layout(doc); - var x = NumCast(doc.x); - var y = NumCast(doc.y); - var w = NumCast(layoutDoc.width); - var h = NumCast(layoutDoc.height); + const layoutDoc = Doc.Layout(doc); + const x = NumCast(doc.x); + const y = NumCast(doc.y); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) { selection.push(doc); } @@ -508,8 +490,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @computed get marqueeDiv() { - let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + const p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; + const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); /** * @RE - The commented out span below * This contains the "C for collection, ..." text on marquees. @@ -521,7 +503,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } render() { - return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> + return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} {this.props.children} </div>; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss new file mode 100644 index 000000000..0c74b8ddb --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -0,0 +1,34 @@ +.collectionMulticolumnView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + + .document-wrapper { + display: flex; + flex-direction: column; + width: 100%; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .multiColumnResizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: column; + + .multiColumnResizer-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/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx new file mode 100644 index 000000000..7d8de0db4 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -0,0 +1,267 @@ +import { action, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from "react"; +import { Doc } from '../../../../new_fields/Doc'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast } 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 { CollectionSubView } from '../CollectionSubView'; +import "./collectionMulticolumnView.scss"; +import ResizeBar from './MulticolumnResizer'; +import WidthLabel from './MulticolumnWidthLabel'; + +type MulticolumnDocument = makeInterface<[typeof documentSchema]>; +const MulticolumnDocument = makeInterface(documentSchema); + +interface WidthSpecifier { + magnitude: number; + unit: string; +} + +interface LayoutData { + widthSpecifiers: WidthSpecifier[]; + starSum: number; +} + +export const DimUnit = { + Pixel: "px", + Ratio: "*" +}; + +const resolvedUnits = Object.values(DimUnit); +const resizerWidth = 8; + +@observer +export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { + + /** + * @returns the list of layout documents whose width unit is + * *, denoting that it will be displayed with a ratio, not fixed pixel, value + */ + @computed + private get ratioDefinedDocs() { + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); + } + + /** + * 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) + * as well as the sum of the * coefficients, i.e. the ratio magnitudes + */ + @computed + private get resolvedLayoutInformation(): LayoutData { + let starSum = 0; + const widthSpecifiers: WidthSpecifier[] = []; + this.childLayoutPairs.map(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); + widthSpecifiers.push({ magnitude, unit }); + } + /** + * Otherwise, the child document is ignored and the remaining + * space is allocated as if the document were absent from the child list + */ + }); + + /** + * Here, since these values are all relative, adjustments during resizing or + * manual updating can, though their ratios remain the same, cause the values + * themselves to drift toward zero. Thus, whenever we change any of the values, + * we normalize everything (dividing by the smallest magnitude). + */ + setTimeout(() => { + const { ratioDefinedDocs } = this; + if (this.childLayoutPairs.length) { + const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1))); + if (minimum !== 0) { + ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1); + } + } + }); + + return { widthSpecifiers, starSum }; + } + + /** + * This returns the total quantity, in pixels, that this + * view needs to reserve for child documents that have + * (with higher priority) requested a fixed pixel width. + * + * If the underlying resolvedLayoutInformation returns null + * because we're waiting on promises to resolve, this value will be undefined as well. + */ + @computed + private get totalFixedAllocation(): number | undefined { + return this.resolvedLayoutInformation?.widthSpecifiers.reduce( + (sum, { magnitude, unit }) => sum + (unit === 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.widthSpecifiers.length; + if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._xMargin); + } + } + + /** + * @returns the total quantity, in pixels, that + * 1* (relative / star unit) is worth. For example, + * if the configuration has three documents, with, respectively, + * widths of 2*, 2* and 1*, and the panel width returns 1000px, + * this accessor returns 1000 / (2 + 2 + 1), or 200px. + * Elsewhere, this is then multiplied by each relative-width + * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). + * + * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined + * because we're waiting indirectly on promises to resolve, this value will be undefined as well. + */ + @computed + private get columnUnitLength(): number | undefined { + if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { + return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; + } + } + + /** + * This wrapper function exists to prevent mobx from + * needlessly rerendering the internal ContentFittingDocumentViews + */ + private getColumnUnitLength = () => this.columnUnitLength; + + /** + * @param layout the document whose transform we'd like to compute + * Given a layout document, this function + * returns the resolved width it has requested, in pixels. + * @returns the stored column width if already in pixels, + * or the ratio width evaluated to a pixel value + */ + private lookupPixels = (layout: Doc): number => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return 0; // we're still waiting on promises to resolve + } + let width = NumCast(layout.dimMagnitude, 1); + if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { + width *= columnUnitLength; + } + return width; + } + + /** + * @returns the transform that will correctly place + * the document decorations box, shifted to the right by + * the sum of all the resolved column widths of the + * documents before the target. + */ + private lookupIndividualTransform = (layout: Doc) => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return Transform.Identity(); // we're still waiting on promises to resolve + } + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.props.ScreenToLocalTransform().translate(-offset, 0); + } + offset += this.lookupPixels(candidate) + resizerWidth; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.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} + 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, 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 = () => this.lookupPixels(layout); + const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + collector.push( + <div className={"document-wrapper"} + key={"wrapper" + i} + style={{ width: width() }} > + {this.getDisplayDoc(layout, dxf, width, height)} + <WidthLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + width={resizerWidth} + key={"resizer" + i} + columnUnitLength={this.getColumnUnitLength} + toLeft={layout} + toRight={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div className={"collectionMulticolumnView_contents"} + 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> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss new file mode 100644 index 000000000..64f607680 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -0,0 +1,35 @@ +.collectionMultirowView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + 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: ew-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..ff7c4998f --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -0,0 +1,269 @@ +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 + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(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} + 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={{ + 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 new file mode 100644 index 000000000..6b89402e6 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -0,0 +1,114 @@ +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 "./CollectionMulticolumnView"; + +interface ResizerProps { + width: number; + columnUnitLength(): number | undefined; + toLeft?: Doc; + toRight?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const { toLeft, toRight, columnUnitLength } = this.props; + const movingRight = movementX > 0; + const toNarrow = movingRight ? toRight : toLeft; + const toWiden = movingRight ? toLeft : toRight; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const 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 scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale); + } + } + } + + private get isActivated() { + const { toLeft, toRight } = this.props; + if (toLeft && toRight) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toLeft) { + if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } else if (toRight) { + if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"multiColumnResizer"} + style={{ + width: this.props.width, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div + className={"multiColumnResizer-hdl"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"multiColumnResizer-hdl"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx new file mode 100644 index 000000000..5b2054428 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { DimUnit } from "./CollectionMulticolumnView"; + +interface WidthLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class WidthLabel extends React.Component<WidthLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.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.showWidthLabels) ? this.contents : (null); + } + +}
\ No newline at end of file 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..d00939b26 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -0,0 +1,114 @@ +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"; + +interface ResizerProps { + height: number; + columnUnitLength(): number | undefined; + toTop?: Doc; + toBottom?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ 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 (this.resizeMode === ResizeMode.Pinned && 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.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + 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, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"multiRowResizer-hdl"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file |
