diff options
| author | Melissa Zhang <mzhang19096@gmail.com> | 2020-09-30 22:01:44 -0600 |
|---|---|---|
| committer | Melissa Zhang <mzhang19096@gmail.com> | 2020-09-30 22:01:44 -0600 |
| commit | 09aab9558a26a2d7c8e3d485aca578960af72821 (patch) | |
| tree | b53bdc6f2fcb269b74a097f56bfeec248e7f918b /src/client/views/collections | |
| parent | bd827b97c719abeadf243ba4f8b2ba417badb65b (diff) | |
| parent | 852ddf70b7ed3d027eb5cb8415df4df77b8652a6 (diff) | |
pull from master
Diffstat (limited to 'src/client/views/collections')
46 files changed, 3270 insertions, 6233 deletions
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 5f8895c1f..652293ed6 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -8,6 +8,8 @@ display: flex; position: absolute; top: 15%; + height: 60%; + width: 100%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 0f3b6f212..4f1ef6e61 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -1,20 +1,18 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { documentSchema, collectionSchema } from '../../../fields/documentSchemas'; +import { Doc } from '../../../fields/Doc'; +import { collectionSchema, documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from '../../../fields/Schema'; -import { NumCast, StrCast, ScriptCast } from '../../../fields/Types'; +import { ScriptField } from '../../../fields/ScriptField'; +import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { returnFalse, Utils, OmitKeys } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import "./CollectionCarousel3DView.scss"; import { CollectionSubView } from './CollectionSubView'; -import { Doc } from '../../../fields/Doc'; -import { ContextMenu } from '../ContextMenu'; -import { ObjectField } from '../../../fields/ObjectField'; -import { returnFalse, Utils } from '../../../Utils'; -import { ScriptField } from '../../../fields/ScriptField'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Id } from '../../../fields/FieldSymbols'; type Carousel3DDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const Carousel3DDocument = makeInterface(documentSchema, collectionSchema); @@ -44,7 +42,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume const displayDoc = (childPair: { layout: Doc, data: Doc }) => { const script = ScriptField.MakeScript("child._showCaption = 'caption'", { child: Doc.name }, { child: childPair.layout }); const onChildClick = script && (() => script); - return <ContentFittingDocumentView {...this.props} + return <ContentFittingDocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} onDoubleClick={this.onChildDoubleClick} onClick={onChildClick} renderDepth={this.props.renderDepth + 1} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 8a27f8102..c5910b0be 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -13,7 +13,7 @@ import { Doc } from '../../../fields/Doc'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { ContextMenu } from '../ContextMenu'; import { ObjectField } from '../../../fields/ObjectField'; -import { returnFalse } from '../../../Utils'; +import { returnFalse, returnZero, OmitKeys } from '../../../Utils'; import { ScriptField } from '../../../fields/ScriptField'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; @@ -45,17 +45,18 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); - return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : + const curDoc = this.childLayoutPairs?.[index]; + return !(curDoc?.layout instanceof Doc) ? (null) : <> <div className="collectionCarouselView-image" key="image"> - <ContentFittingDocumentView {...this.props} + <ContentFittingDocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} - Document={this.childLayoutPairs[index].layout} - DataDoc={this.childLayoutPairs[index].data} + Document={curDoc.layout} + DataDoc={curDoc.data} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.props.ScreenToLocalTransform} bringToFront={returnFalse} @@ -71,7 +72,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) <FormattedTextBox key={index} {...this.props} xMargin={NumCast(this.layoutDoc["_carousel-caption-xMargin"])} yMargin={NumCast(this.layoutDoc["_carousel-caption-yMargin"])} - Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"} /> + Document={curDoc.layout} DataDoc={undefined} fieldKey={"caption"} /> </div> </>; } diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 6ebd5103b..8b1594b21 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,24 +1,5 @@ @import "../../views/globalCssVariables.scss"; -.miniMap { - position: absolute; - overflow: hidden; - right: 10; - bottom: 10; - border: solid 1px; - box-shadow: black 0.4vw 0.4vw 0.8vw; - - .miniOverlay { - width: 100%; - height: 100%; - position: absolute; - - .miniThumb { - background: #25252525; - position: absolute; - } - } -} .lm_title { margin-top: 3px; @@ -54,6 +35,7 @@ padding-right: 20px; margin-top: -1px; border-bottom: 1px black; + .collectionDockingView-gear { display: none; } @@ -63,25 +45,14 @@ padding-right: 20px; margin-top: 1px; border-bottom: unset; + .collectionDockingView-gear { display: inline-block; } } .lm_popout { - display: none; -} - -.messageCounter { - width: 18px; - height: 20px; - text-align: center; - border-radius: 20px; - margin-left: 5px; - transform: translate(0px, -8px); - display: inline-block; - background: transparent; - border: 1px #999999 solid; + display: inline; } .collectiondockingview-container { @@ -101,7 +72,7 @@ margin: auto; } - .collectionDockingView-dragAsDocument { + .collectionDockingView-drag { touch-action: none; position: absolute; padding-left: 5px; @@ -119,6 +90,10 @@ transform: scale(1.2); } + .lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC) + } + .lm_maximised .lm_controls .lm_maximise { opacity: 1; transform: scale(0.8); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 43da0d3cf..2eaa284cc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,46 +1,33 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import * as GoldenLayout from "../../../client/goldenLayout"; -import { DateField } from '../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; -import { FieldId } from "../../../fields/RefField"; +import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse, emptyPath, aggregateBounds } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager, dropActionType } from "../../util/DragManager"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DragManager } from "../../util/DragManager"; +import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; -import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch } from "../../util/UndoManager"; -import { MainView } from '../MainView'; -import { DocumentView } from "../nodes/DocumentView"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import "./CollectionDockingView.scss"; -import { SubCollectionViewProps } from "./CollectionSubView"; -import { DockingViewButtonSelector } from './ParentDocumentSelector'; -import React = require("react"); +import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; -import { SnappingManager } from '../../util/SnappingManager'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { listSpec } from '../../../fields/Schema'; -import { clamp } from 'lodash'; -import { PresBox } from '../nodes/PresBox'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { InteractionUtils } from '../../util/InteractionUtils'; -import { InkTool } from '../../../fields/InkField'; +import { TabDocView } from './TabDocView'; +import React = require("react"); +import { stat } from 'fs'; const _global = (window /* browser */ || global /* node */) as any; @observer -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, width?: number, libraryPath?: Doc[]) { +export class CollectionDockingView extends CollectionSubView(doc => doc) { + @observable public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -48,341 +35,219 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - libraryPath: libraryPath?.map(d => d[Id]) + panelName // name of tab that can be used to close or replace its contents } }; } - @computed public get initialized() { - return this._goldenLayout !== null; - } - - @observable private _goldenLayout: any = null; + private _reactionDisposer?: IReactionDisposer; private _containerRef = React.createRef<HTMLDivElement>(); - private _flush: boolean = false; + private _flush: UndoManager.Batch | undefined; private _ignoreStateChange = ""; - private _isPointerDown = false; - private _maximizedSrc: Opt<DocumentView>; + public tabMap: Set<any> = new Set(); + public get initialized() { return this._goldenLayout !== null; } + public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } + @observable private _goldenLayout: any = null; constructor(props: SubCollectionViewProps) { super(props); - runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this)); + runInAction(() => CollectionDockingView.Instance = this); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; DragManager.StartWindowDrag = this.StartOtherDrag; } + public StartOtherDrag = (e: any, dragDocs: Doc[]) => { - let config: any; - if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0]); - } else { - config = { - type: 'row', - content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc); - }) - }; - } - const div = document.createElement("div"); - const dragSource = this._goldenLayout.createDragSource(div, config); - dragSource._dragListener.on("dragStop", () => { - dragSource.destroy(); - }); + !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag")); + const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : + { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) }; + const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config); + //dragSource._dragListener.on("dragStop", dragSource.destroy); dragSource._dragListener.onMouseDown(e); } @undoBatch - @action - public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { - if (docView.props.Document._viewType === CollectionViewType.Docking && docView.props.Document.layoutKey === "layout") { - return MainView.Instance.openWorkspace(docView.props.Document); - } - const document = Doc.MakeAlias(docView.props.Document); - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - this._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - this._goldenLayout._$maximiseItem(docconfig); - this._maximizedSrc = docView; - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + public CloseFullScreen = () => { + this._goldenLayout._maximisedItem?.toggleMaximise(); this.stateChanged(); - SelectionManager.DeselectAll(); } - public CloseFullScreen = () => { - const target = this._goldenLayout._maximisedItem; - if (target !== null && this._maximizedSrc) { - this._goldenLayout._maximisedItem.remove(); - SelectionManager.SelectDoc(this._maximizedSrc, false); - this._maximizedSrc = undefined; - this.stateChanged(); + @undoBatch + public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean { + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document); + if (tab) { + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + if (j !== -1) { + tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); + } } - } - public HasFullScreen = () => { - return this._goldenLayout._maximisedItem !== null; + return false; } @undoBatch - @action - public static CloseRightSplit(document: Opt<Doc>): boolean { + public static OpenFullScreen(doc: Doc, libraryPath?: Doc[]) { const instance = CollectionDockingView.Instance; - const tryClose = (childItem: any) => { - if (childItem.config?.component === "DocumentFrameRenderer") { - const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId); - if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) { - childItem.remove(); - instance.layoutChanged(document); - return true; - } - } - return false; + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { + return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + } + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))] }; - const retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false : - Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose)); - - retVal && instance.stateChanged(); - return retVal; + const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + instance._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + instance._goldenLayout._$maximiseItem(docconfig); + instance._goldenLayout.emit('stateChanged'); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + instance.stateChanged(); + return true; } - @action - layoutChanged(removed?: Doc) { - this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('stateChanged'); - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); - if (removed) CollectionDockingView.Instance._removedDocs.push(removed); - this.stateChanged(); - } @undoBatch - @action - public static ReplaceRightSplit(document: Doc, libraryPath?: Doc[], addToSplit?: boolean): boolean { - if (!CollectionDockingView.Instance) return false; + public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { 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, 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, undefined, libraryPath); - child.addChild(newItemStackConfig, undefined); - !addToSplit && child.contentItems[j].remove(); - instance.layoutChanged(document); - return true; - } - return false; - }); - }); + if (!instance) return false; + const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); + if (!panelName && stack) { + const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); + const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + stack.contentItems[activeContentItemIndex].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - if (retVal) { - instance.stateChanged(); + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => tab.contentItem.config.props.panelName === panelName); + if (tab) { + tab.header.parent.addChild(newConfig, undefined); + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - return retVal; + return CollectionDockingView.AddSplit(document, panelName, stack, panelName); } - // - // 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, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; - const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else { - 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); - newRow.addChild(collayout, 0, true); - - collayout.config.width = 50; - newContentItem.config.width = 50; - } - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; + @undoBatch + public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { + return Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex((tab) => tab.DashDoc === doc) !== -1 ? + CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); } - // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @undoBatch - @action - public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; + public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document); const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row - if (pullSide === "left") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "right") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "top" || pullSide === "bottom") { - // if not going in a row layout, must add already existing content into column - const rowlayout = instance._goldenLayout.root.contentItems[0]; - const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); - rowlayout.parent.replaceChild(rowlayout, newColumn); - if (pullSide === "top") { - newColumn.addChild(rowlayout, undefined, true); - newColumn.addChild(newContentItem, 0, true); - } else if (pullSide === "bottom") { - newColumn.addChild(newContentItem, undefined, true); - newColumn.addChild(rowlayout, 0, true); - } + if (!instance) return false; + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); - rowlayout.config.height = 50; - newContentItem.config.height = 50; - } - } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column - if (pullSide === "top") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "bottom") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "left" || pullSide === "right") { - // if not going in a row layout, must add already existing content into column - const collayout = instance._goldenLayout.root.contentItems[0]; - const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); - - if (pullSide === "left") { - newRow.addChild(collayout, undefined, true); - newRow.addChild(newContentItem, 0, true); - } else if (pullSide === "right") { - newRow.addChild(newContentItem, undefined, true); - newRow.addChild(collayout, 0, true); - } - - collayout.config.width = 50; - newContentItem.config.width = 50; + if (!pullSide && stack) { + stack.addChild(docContentConfig, undefined); + stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]); + } else { + const newItemStackConfig = { type: 'stack', content: [docContentConfig] }; + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isStack) { + instance._goldenLayout.root.contentItems[0].addChild(docContentConfig); + } else if (instance._goldenLayout.root.contentItems.length === 1 && instance._goldenLayout.root.contentItems[0].contentItems.length === 1 && + instance._goldenLayout.root.contentItems[0].contentItems[0].contentItems.length === 0) { + instance._goldenLayout.root.contentItems[0].contentItems[0].addChild(docContentConfig); } - } - - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; - } - + else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row + switch (pullSide) { + default: + case "right": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "top": + case "bottom": + // if not going in a row layout, must add already existing content into column + const rowlayout = instance._goldenLayout.root.contentItems[0]; + const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + rowlayout.parent.replaceChild(rowlayout, newColumn); + if (pullSide === "top") { + newColumn.addChild(rowlayout, undefined, true); + newColumn.addChild(newContentItem, 0, true); + } else if (pullSide === "bottom") { + newColumn.addChild(newContentItem, undefined, true); + newColumn.addChild(rowlayout, 0, true); + } - // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split - // - @undoBatch - @action - public static UseRightSplit(document: Doc, libraryPath?: Doc[], shiftKey?: boolean) { - document.isDisplayPanel = true; - if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, libraryPath, shiftKey)) { - CollectionDockingView.AddRightSplit(document, libraryPath); - } - } + rowlayout.config.height = 50; + newContentItem.config.height = 50; + } + } else {// if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column + switch (pullSide) { + case "top": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "bottom": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": + case "right": + default: + // if not going in a row layout, must add already existing content into column + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + if (pullSide === "left") { + newRow.addChild(collayout, undefined, true); + newRow.addChild(newContentItem, 0, true); + } else { + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + } - @undoBatch - @action - public AddTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; + collayout.config.width = 50; + newContentItem.config.width = 50; } } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + newContentItem.callDownwards('_$init'); } - this.layoutChanged(); - return true; + + return instance.layoutChanged(); } @undoBatch @action - public ReplaceTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; - } - } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); - } - this.layoutChanged(); + layoutChanged() { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + this.stateChanged(); return true; } - setupGoldenLayout() { + async setupGoldenLayout() { const config = StrCast(this.props.Document.dockingConfig); if (config) { - if (!this._goldenLayout) { - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); - } - else { + const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) ?? []; + await Promise.all(docids.map(id => DocServer.GetRefField(id))); + + if (this._goldenLayout) { if (config === JSON.stringify(this._goldenLayout.toConfig())) { return; + } else { + try { + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } } - try { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - } catch (e) { } - this._goldenLayout.destroy(); - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); } - this._goldenLayout.on('itemDropped', this.itemDropped); + this.tabMap.clear(); + this._goldenLayout?.destroy(); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); this._goldenLayout.on('tabCreated', this.tabCreated); this._goldenLayout.on('tabDestroyed', this.tabDestroyed); this._goldenLayout.on('stackCreated', this.stackCreated); - this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.registerComponent('DocumentFrameRenderer', TabDocView); this._goldenLayout.container = this._containerRef.current; if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { try { @@ -394,582 +259,156 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } - reactionDisposer?: Lambda; + componentDidMount: () => void = () => { if (this._containerRef.current) { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this.onResize(null as any); - } - })); - observer.observe(this._containerRef.current); - this.reactionDisposer = reaction( - () => 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); - DocListCast((Doc.UserDoc().myWorkspaces as Doc).data).map(d => d.workspaceBrush = false); - this.props.Document.workspaceBrush = true; + new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); + this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig), + config => { + if (!this._goldenLayout || this._ignoreStateChange !== config) { // bcz: TODO! really need to diff config with ignoreStateChange and modify the current goldenLayout instead of building a new one. + this.setupGoldenLayout(); } this._ignoreStateChange = ""; - }, { fireImmediately: true }); - - window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + }); + setTimeout(() => this.setupGoldenLayout(), 0); + //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } + componentWillUnmount: () => void = () => { try { - this.props.Document.workspaceBrush = false; - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('stackCreated', this.stackCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - } catch (e) { - - } - this._goldenLayout && this._goldenLayout.destroy(); - runInAction(() => { - CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1); - this._goldenLayout = null; - }); + } catch (e) { } + this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); - this.reactionDisposer && this.reactionDisposer(); + this._reactionDisposer?.(); } + @action onResize = (event: any) => { 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?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); } @action - onPointerUp = (e: React.PointerEvent): void => { + onPointerUp = (e: MouseEvent): void => { + window.removeEventListener("pointerup", this.onPointerUp); if (this._flush) { - this._flush = false; setTimeout(() => { CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); this.stateChanged(); + this._flush?.end(); + this._flush = undefined; }, 10); } } + @action onPointerDown = (e: React.PointerEvent): void => { - this._isPointerDown = true; - const onPointerUp = action(() => { - window.removeEventListener("pointerup", onPointerUp); - this._isPointerDown = false; - }); - window.addEventListener("pointerup", onPointerUp); - const className = (e.target as any).className; - if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { - this._flush = true; + window.addEventListener("mouseup", this.onPointerUp); + if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) { + this._flush = UndoManager.StartBatch("golden layout edit"); } - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { - return; - } else { + if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && + Doc.GetSelectedTool() !== InkTool.Highlighter && Doc.GetSelectedTool() !== InkTool.Pen) { e.stopPropagation(); } } - updateDataField = async (json: string) => { + public static Copy(doc: Doc) { + let json = StrCast(doc.dockingConfig); const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); - const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) || []; + const docs = docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); + const newtabs = docs.map(doc => { + const copy = Doc.MakeAlias(doc); + json = json.replace(doc[Id], copy[Id]); + return copy; + }); + const copy = Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title }); + const docsublists = DocListCast(doc.data); + const copysublists = DocListCast(copy.data); + const docother = Cast(docsublists[1], Doc, null); + const copyother = Cast(copysublists[1], Doc, null); + const newother = DocListCast(docother.data).map(doc => Doc.MakeAlias(doc)); + Doc.GetProto(copyother).data = new List<Doc>(newother); - if (docids) { - const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); - docs.map(doc => Doc.AddDocToList(Doc.GetProto(this.props.Document), this.props.fieldKey, doc)); - // Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); - } + return copy; } - @undoBatch + @action stateChanged = () => { const json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.dockingConfig = json; - this.updateDataField(json); - } - - itemDropped = () => { - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - this.stateChanged(); - } - - htmlToElement(html: string) { - 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; - } - - tabCreated = async (tab: any) => { - tab.titleElement[0].Tab = tab; - if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - if (tab.contentItem.config.fixed) { - tab.contentItem.parent.config.fixed = true; - } + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; - if (doc instanceof Doc) { - tab.titleElement[0].onclick = (e: any) => { - if (Date.now() - tab.titleElement[0].lastClick < 1000) tab.titleElement[0].select(); - tab.titleElement[0].lastClick = Date.now(); - tab.titleElement[0].focus(); - }; - tab.titleElement[0].onchange = (e: any) => { - tab.titleElement[0].size = e.currentTarget.value.length + 1; - Doc.GetProto(doc).title = e.currentTarget.value, true; - }; - tab.titleElement[0].size = StrCast(doc.title).length + 1; - tab.titleElement[0].value = doc.title; - tab.titleElement[0].style["max-width"] = "100px"; - const gearSpan = document.createElement("span"); - gearSpan.className = "collectionDockingView-gear"; - gearSpan.style.position = "relative"; - gearSpan.style.paddingLeft = "0px"; - gearSpan.style.paddingRight = "12px"; - const stack = tab.contentItem.parent; - tab.element[0].onpointerdown = (e: any) => { - const view = DocumentManager.Instance.getDocumentView(doc); - view && SelectionManager.SelectDoc(view, false); - }; - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; - const activeContentItem = tab.header.parent.getActiveContentItem(); - if (tab.contentItem !== activeContentItem) { - tab.header.parent.setActiveContentItem(tab.contentItem); - } - tab.setActive(true); - }; - const onDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e) => { - if (!(e as any).defaultPrevented) { - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction as dropActionType; - DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - return true; - } - return false; - }, returnFalse, emptyFunction); - }; - - tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), - (views) => { - if (views.length) { - ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > - <DockingViewButtonSelector views={() => views} Stack={stack} /> - </span>, - gearSpan); - tab.buttonDisposer?.(); - } - }, { fireImmediately: true }); - - tab.reactComponents = [gearSpan]; - tab.element.append(gearSpan); - tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { - tab.titleElement[0].textContent = title, { fireImmediately: true }; - tab.titleElement[0].style.padding = degree ? 0 : 2; - tab.titleElement[0].style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; - }); - //TODO why can't this just be doc instead of the id? - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - } - tab.closeElement.off('click') //unbind the current click handler - .click(async function () { - tab.reactionDisposer?.(); - tab.buttonDisposer?.(); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); - if (doc instanceof Doc) { - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - - const recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc); - if (recent) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - SelectionManager.DeselectAll(); - } - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - tab.contentItem.remove(); - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - }); + this.props.Document.dockingConfig = json; + const sublists = DocListCast(this.props.Document[this.props.fieldKey]); + const tabs = Cast(sublists[0], Doc, null); + const other = Cast(sublists[1], Doc, null); + const tabdocs = DocListCast(tabs.data); + const otherdocs = DocListCast(other.data); + Doc.GetProto(tabs).data = new List<Doc>(docs); + const otherSet = new Set<Doc>(); + otherdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + tabdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + Doc.GetProto(other).data = new List<Doc>(Array.from(otherSet.values())); } tabDestroyed = (tab: any) => { - if (tab.reactComponents) { - for (const ele of tab.reactComponents) { - ReactDOM.unmountComponentAtNode(ele); - } - } + this.tabMap.delete(tab); + tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + } + tabCreated = (tab: any) => { + tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content) } - _removedDocs: Doc[] = []; stackCreated = (stack: any) => { - //stack.header.controlsContainer.find('.lm_popout').hide(); - 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" })); + stack.header?.element.on('mousedown', (e: any) => { + if (e.target === stack.header?.element[0] && e.button === 2) { + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); } }); - // 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 + stack.header?.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(action(async function () { + .click(action(() => { //if (confirm('really close this?')) { - - stack.remove(); - stack.contentItems.forEach(async (contentItem: any) => { - const doc = await DocServer.GetRefField(contentItem.config.props.documentId); - if (doc instanceof Doc) { - let recent: Doc | undefined; - if (recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc)) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - } - }); - //} + if (!stack.parent.parent.isRoot || stack.parent.contentItems.length > 1) { + stack.remove(); + stack.contentItems.forEach((contentItem: any) => Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", contentItem.tab.DashDoc, undefined, true, true)); + } else { + alert('cant delete the last stack'); + } })); - stack.header.controlsContainer.find('.lm_popout') //get the close icon + stack.header?.controlsContainer.find('.lm_popout') //get the close icon .off('click') //unbind the current click handler - .click(action(function () { - stack.config.fixed = !stack.config.fixed; - // 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"); + .click(action(() => { + // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); })); } render() { - if (this.props.renderDepth > 0) { - return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; - } - return <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />; - } - -} - -interface DockedFrameProps { - documentId: 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>; - @observable private _isActive: boolean = false; - _tabReaction: IReactionDisposer | undefined; - - get _stack(): any { - return (this.props as any).glContainer.parent.parent; - } - get _tab(): any { - const tab = (this.props as any).glContainer.tab?.element[0] as HTMLElement; - return tab?.getElementsByClassName("lm_title")?.[0]; - } - constructor(props: any) { - super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = 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))); - } - - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static PinDoc(doc: Doc, unpin = false) { - if (unpin) DockedFrameRenderer.UnpinDoc(doc); - else { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.presZoomButton = true; - pinDoc.context = curPres; - Doc.AddDocToList(curPres, "data", pinDoc); - if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - if (!DocumentManager.Instance.getDocumentView(curPres)) { - CollectionDockingView.AddRightSplit(curPres); - } - DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); - } - } - } - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static UnpinDoc(doc: Doc) { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); - ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); - } - } - - componentDidMount() { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this._panelWidth = entry.contentRect.width; - this._panelHeight = entry.contentRect.height; - } - })); - observer.observe(this.props.glContainer._element[0]); - this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.on("tab", this.onActiveContentItemChanged); - this.onActiveContentItemChanged(); - this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: StrCast(this._document?._backgroundColor, this._document && CollectionDockingView.Instance?.props.backgroundColor?.(this._document, 0) || "white") }), - (data) => { - const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); - this._tab && (this._tab.style.backgroundColor = selected ? data.color : ""); - } - ); - } - - componentWillUnmount() { - this._tabReaction?.(); - this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.off("tab", this.onActiveContentItemChanged); - } - - @action.bound - private onActiveContentItemChanged() { - if (this.props.glContainer.tab) { - this._isActive = this.props.glContainer.tab.isActive; - setTimeout(() => { - const dv = this._document && DocumentManager.Instance.getFirstDocumentView(this._document); - dv && SelectionManager.SelectDoc(dv, false); - }); - !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. - } - } - - get layoutDoc() { return this._document && Doc.Layout(this._document); } - nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; - panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : - (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) - panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; - - nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; - - contentScaling = () => { - const nativeH = this.nativeHeight(); - const nativeW = this.nativeWidth(); - let scaling = 1; - if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { - scaling = 1; - } else if (NumCast(this.layoutDoc!._nativeWidth) && ((this.layoutDoc?._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth))) { - scaling = this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - } else if (nativeW && nativeH) { - // if (this.layoutDoc!.type === DocumentType.PDF || this.layoutDoc!.type === DocumentType.WEB) { - // if ((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); - // } - // } - const wscale = this.panelWidth() / nativeW; - scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } else scaling = 1; - return scaling; - } - - ScreenToLocalTransform = () => { - if (this._mainCont && this._mainCont.children) { - 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._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}%` : undefined; } - - addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { - SelectionManager.DeselectAll(); - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { - return MainView.Instance.openWorkspace(doc); - } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, libraryPath); - } else if (location === "close") { - return CollectionDockingView.CloseRightSplit(doc); - } else if (location === "replace") { - CollectionDockingView.UseRightSplit(doc); - return true; - } else {// if (location === "inPlace") { - return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); - } - } - - @computed get renderContentBounds() { - const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; - const xbounds = bounds[2] - bounds[0]; - const ybounds = bounds[3] - bounds[1]; - const dim = Math.max(xbounds, ybounds); - return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; - } - @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; } - @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; } - @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim); - this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim); - return false; - }), emptyFunction, emptyFunction); - } - getCurrentFrame = (): number => { - const presTargetDoc = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null); - const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); - return currentFrame; - } - - renderMiniMap() { - return <div className="miniMap" style={{ - width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, - StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!, 0))), - }}> - <CollectionFreeFormView - Document={this._document!} - LibraryPath={emptyPath} - CollectionView={undefined} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this. - noOverlay={true} // don't render overlay Docs since they won't scale - active={returnTrue} - select={emptyFunction} - dropAction={undefined} - isSelected={returnFalse} - dontRegisterView={true} - annotationsKey={""} - fieldKey={Doc.LayoutFieldKey(this._document!)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - ContentScaling={returnOne} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - NativeHeight={returnZero} - NativeWidth={returnZero} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - fitToBox={true} - /> - <div className="miniOverlay" onPointerDown={this.miniDown} > - <div className="miniThumb" style={{ - width: `${this.miniWidth}%`, - height: `${this.miniHeight}%`, - left: `${this.miniLeft}%`, - top: `${this.miniTop}%`, - }} - /> - </div> + return <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef}> + {this.props.renderDepth > 0 ? "Nested dashboards can't be rendered" : (null)} </div>; } - @computed get docView() { - TraceMobx(); - if (!this._document) return (null); - const document = this._document; - const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined; - return <> - <DocumentView key={document[Id]} - LibraryPath={this._libraryPath} - Document={document} - DataDoc={resolvedDataDoc} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - NativeHeight={this.nativeHeight} - NativeWidth={this.nativeWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} - </>; - } - - render() { - return (!this._isActive || !this.layoutDoc) ? (null) : - (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} - style={{ - transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", - width: this.widthpercent - }}> - {this.docView} - </div >); - } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddSplit(doc, "right"); }, "opens up the inputted document on the right side of the screen", "(doc: any)"); -Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/CollectionDockingViewMenu.scss index bc9cf4848..4157f0f7e 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/CollectionDockingViewMenu.scss @@ -1,15 +1,14 @@ -.parentDocumentSelector-linkFlyout { + +.dockingViewButtonSelector { div { overflow: visible !important; } - .metadataEntry-outerDiv { - overflow: hidden !important; - pointer-events: all; - } + display: inline-block; + width:100%; + height:100%; } - -.parentDocumentSelector-flyout { +.dockingViewButtonSelector-flyout { position: relative; z-index: 9999; background-color: #eeeeee; @@ -32,32 +31,4 @@ border-right: 0px; border-left: 0px; } -} - -.parentDocumentSelector-button { - pointer-events: all; - position: relative; - display: inline-block; - - svg { - // width:20px !important; - //height:20px; - } -} - -.parentDocumentSelector-metadata { - pointer-events: auto; - padding-right: 5px; - width: 25px; - display: inline-block; -} - -.buttonSelector { - div { - overflow: visible !important; - } - - display: inline-block; - width:100%; - height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingViewMenu.tsx b/src/client/views/collections/CollectionDockingViewMenu.tsx new file mode 100644 index 000000000..1cab293a8 --- /dev/null +++ b/src/client/views/collections/CollectionDockingViewMenu.tsx @@ -0,0 +1,48 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentView } from "../nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class CollectionDockingViewMenu extends React.Component<{ views: () => DocumentView[], Stack: any }> { + customStylesheet(styles: any) { + return { + ...styles, + panel: { + ...styles.panel, + minWidth: "100px" + }, + }; + } + _ref = React.createRef<HTMLDivElement>(); + + @computed get flyout() { + return ( + <div className="dockingViewButtonSelector-flyout" title=" " ref={this._ref}> + <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> + </div> + ); + } + + @observable _tooltipOpen: boolean = false; + render() { + return <Tooltip open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> + <span className="dockingViewButtonSelector" + onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} + onPointerDown={action(e => { + this.props.views()[0]?.select(false); + this._tooltipOpen = false; + })} > + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> + <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> + </Flyout> + </span> + </Tooltip >; + } +} diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index f5c4299a9..ca72b98a5 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -4,10 +4,12 @@ .collectionLinearView-outer { overflow: visible; height: 100%; + pointer-events: none; .collectionLinearView { display: flex; height: 100%; + align-items: center; >span { background: $dark-color; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index e1b07077e..859ee9362 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -1,22 +1,21 @@ +import { Tooltip } from '@material-ui/core'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from '../../../fields/Schema'; -import { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse, returnZero } from '../../../Utils'; +import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, 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 '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; -import { Tooltip } from '@material-ui/core'; +import "./CollectionLinearView.scss"; +import { CollectionSubView } from './CollectionSubView'; +import { CollectionViewType } from './CollectionView'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -89,6 +88,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } @action @@ -112,12 +112,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); const color = StrCast(this.props.Document.color, "white"); - const menuOpener = <label htmlFor={`${guid}`} style={{ - background: backgroundColor === color ? "black" : backgroundColor, - // width: "18px", height: "18px", fontSize: "12.5px", - // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s", - // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)" - }} + const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "pointer", background: backgroundColor === color ? "black" : backgroundColor, }} onPointerDown={e => e.stopPropagation()} > <p>{BoolCast(this.props.Document.linearViewIsExpanded) ? "–" : "+"}</p> </label>; @@ -128,21 +123,22 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { {menuOpener} </Tooltip> <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} - onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> + onChange={action(() => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> {this.childLayoutPairs.map((pair, ind) => { const nested = pair.layout._viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); - const deltaSize = nativeWidth * .15 / 2; const scalable = pair.layout.onClick || pair.layout.onDragStart; return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ - width: scalable ? (nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize) : undefined, - height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + pointerEvents: "all", + minWidth: 30, + width: nested ? pair.layout[WidthSym]() : this.dimension(), + height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension(), }} > - <DocumentView + <ContentFittingDocumentView Document={pair.layout} DataDoc={pair.data} LibraryPath={this.props.LibraryPath} @@ -155,10 +151,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing - PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} + PanelWidth={nested ? pair.layout[WidthSym] : this.dimension} + PanelHeight={nested ? pair.layout[HeightSym] : this.dimension} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} backgroundColor={this.props.backgroundColor} @@ -166,13 +160,16 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; })} </div> {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ - background: backgroundColor === color ? "black" : backgroundColor + background: backgroundColor === color ? "black" : backgroundColor, + pointerEvents: "all" }} onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > @@ -192,9 +189,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { </span> </Tooltip> - {/* <FontAwesomeIcon icon="times-circle" size="lg" style={{ color: "red" }} - onClick={this.exitLongLinks} /> */} - </span> : null} </div> </div>; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c772dcfe7..7014966c7 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; +import { Doc, DocListCast, DataSym } from "../../../fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; import { StrCast, NumCast } from "../../../fields/Types"; @@ -90,7 +90,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(this.heading); - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); + const onLayoutDoc = this.onLayoutDoc(key); + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); this.props.parent.onInternalDrop(e, de); e.stopPropagation(); } @@ -115,7 +116,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr return false; } } - this.props.docList.forEach(d => d[key] = castedValue); + this.props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); this._heading = castedValue.toString(); return true; } @@ -141,7 +142,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value }); - newDoc[key] = this.getValue(this.props.heading); + const onLayoutDoc = this.onLayoutDoc(key); + (onLayoutDoc ? newDoc : newDoc[DataSym])[key] = this.getValue(this.props.heading); const docs = this.props.parent.childDocList; return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc); } @@ -149,7 +151,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr deleteRow = undoBatch(action(() => { this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); - this.props.docList.forEach(d => d[key] = undefined); + this.props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); if (this.props.parent.columnHeaders && this.props.headingObject) { const index = this.props.parent.columnHeaders.indexOf(this.props.headingObject); this.props.parent.columnHeaders.splice(index, 1); @@ -180,11 +182,21 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { - setupMoveUpEvents(this, e, this.headerMove, emptyFunction, () => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e)); + setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e)); this._createAliasSelected = false; } } + /** + * Returns true if a key is on the layout doc of the documents in the collection. + */ + onLayoutDoc = (key: string): boolean => { + DocListCast(this.props.parent.Document.data).forEach(doc => { + if (Doc.Get(doc, key, true)) return true; + }); + return false; + } + renderColorPicker = () => { const selected = this.color; diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 2f6d97f99..e36e5caa7 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -229,18 +229,18 @@ .flexLabel { margin-bottom: 0; } - } - .collectionGridViewChrome-entryBox { - width: 50%; - } + .collectionGridViewChrome-entryBox { + width: 50%; + color: black; + } - .collectionGridViewChrome-columnButton { - color: black; + .collectionGridViewChrome-columnButton { + color: black; + } } } - .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; @@ -315,6 +315,59 @@ } } +.webBox-urlEditor { + position: relative; + opacity: 0.9; + z-index: 9001; + transition: top .5s; + + .urlEditor { + display: grid; + grid-template-columns: 1fr auto; + padding-bottom: 10px; + overflow: hidden; + margin-top: 5px; + height: 35px; + + .editorBase { + display: flex; + + .editor-collapse { + transition: all .5s, opacity 0.3s; + position: absolute; + width: 40px; + transform-origin: top left; + } + + .switchToText { + color: $main-accent; + } + + .switchToText:hover { + color: $dark-color; + } + } + + button:hover { + transform: scale(1); + } + } +} + +.webpage-urlInput { + padding: 12px 10px 11px 10px; + border: 0px; + color: black; + font-size: 10px; + letter-spacing: 2px; + outline-color: black; + background: rgb(238, 238, 238); + width: 100%; + min-width: 350px; + margin-right: 10px; + height: 100%; +} + .collectionFreeFormMenu-cont { display: inline-flex; position: relative; @@ -324,6 +377,7 @@ .antimodeMenu-button { text-align: center; display: block; + position: relative; } .color-previewI { @@ -331,12 +385,15 @@ height: 20%; bottom: 0; position: absolute; + margin-left: 2px; } .color-previewII { width: 80%; height: 80%; margin-left: 10%; + position: absolute; + bottom: 5; } .btn-group { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 25a2fee04..51d1d5559 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -5,35 +5,39 @@ import { Tooltip } from "@material-ui/core"; import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Document } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; -import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; +import { WebField } from "../../../fields/URLField"; import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; +import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { undoBatch } from "../../util/UndoManager"; -import AntimodeMenu, { AntimodeMenuProps } from "../AntimodeMenu"; +import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; -import GestureOverlay from "../GestureOverlay"; +import { GestureOverlay } from "../GestureOverlay"; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; -import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; +import { PresBox } from "../nodes/PresBox"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { TabDocView } from "./TabDocView"; +import { RichTextField } from "../../../fields/RichTextField"; @observer -export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: CollectionMenu; +export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { + @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined; @observable FieldKey: string; @@ -41,9 +45,9 @@ export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: any) { super(props); this.FieldKey = ""; - CollectionMenu.Instance = this; + runInAction(() => CollectionMenu.Instance = this); this._canFade = false; // don't let the inking menu fade away - this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true); + runInAction(() => this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true)); this.jumpTo(300, 300); } @@ -91,13 +95,13 @@ export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip>; - return this.getElement(!this.SelectedCollection ? [button] : + return this.getElement(!this.SelectedCollection ? [/*button*/] : [<CollectionViewBaseChrome key="chrome" docView={this.SelectedCollection} - fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)} + fieldKey={this.SelectedCollection.LayoutFieldKey} type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, prop, - button]); + /*button*/]); } } @@ -118,7 +122,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp _templateCommand = { params: ["target", "source"], title: "item view", script: "self.target.childLayoutTemplate = getDocTemplate(self.source?.[0])", - immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]))), + immediate: undoBatch((source: Doc[]) => { + let formatStr = source.length && Cast(source[0].text, RichTextField, null)?.Text; + try { formatStr && JSON.parse(formatStr); } catch (e) { formatStr = ""; } + if (source.length === 1 && formatStr) { + Doc.SetInPlace(this.target, "childLayoutString", formatStr, false); + } else if (source.length) { + this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]); + } else { + Doc.SetInPlace(this.target, "childLayoutString", undefined, true); + Doc.SetInPlace(this.target, "childLayoutTemplate", undefined, true); + } + }), initialize: emptyFunction, }; _narrativeCommand = { @@ -130,7 +145,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp _contentCommand = { params: ["target", "source"], title: "set content", script: "getProto(self.target).data = copyField(self.source);", - immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), + immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), initialize: emptyFunction, }; _onClickCommand = { @@ -161,9 +176,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _viewCommand = { params: ["target"], title: "bookmark view", - script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale'];", - immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }), - initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; }, + script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale']; gotoFrame(self.target, self['target-currentFrame']);", + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; this.target._currentFrame = 0; }), + initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; button['target-currentFrame'] = this.target._currentFrame; }, }; _clusterCommand = { params: ["target"], title: "fit content", @@ -173,18 +188,23 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _fitContentCommand = { params: ["target"], title: "toggle clusters", - script: "self.target.useClusters = !self.target.useClusters;", - immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), + script: "self.target._useClusters = !self.target._useClusters;", + immediate: undoBatch((source: Doc[]) => this.target._useClusters = !this.target._useClusters), initialize: emptyFunction }; _saveFilterCommand = { params: ["target"], title: "save filter", - script: "self.target._docFilters = copyField(self['target-docFilters']);", - immediate: undoBatch((source: Doc[]) => this.target._docFilters = undefined), - initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; }, + script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']); + self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, + immediate: undoBatch((source: Doc[]) => { this.target._docFilters = undefined; this.target._searchFilterDocs = undefined; }), + initialize: (button: Doc) => { + button['target-docFilters'] = (Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) instanceof ObjectField ? + ObjectField.MakeCopy((Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) as any as ObjectField) : undefined; + button['target-searchFilterDocs'] = CurrentUserUtils.ActiveDashboard._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(CurrentUserUtils.ActiveDashboard._searchFilterDocs as any as ObjectField) : undefined; + }, }; - @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } @@ -223,18 +243,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } - @action - toggleViewSpecs = (e: React.SyntheticEvent) => { - this.document._facetWidth = this.document._facetWidth ? 0 : 200; - e.stopPropagation(); - } @action closeViewSpecs = () => { this.document._facetWidth = 0; } @computed get subChrome() { - switch (this.props.type) { + switch (this.props.docView.props.LayoutTemplateString ? CollectionViewType.Freeform : this.props.type) { // bcz: ARgh! hack to get menu for tree view outline items default: return this.otherSubChrome; case CollectionViewType.Invalid: case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />); @@ -326,6 +341,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @computed get viewModes() { + const excludedViewTypes = Doc.UserDoc().noviceMode ? [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Map, CollectionViewType.Linear, CollectionViewType.Time] : + [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Linear]; return <div className="collectionViewBaseChrome-viewModes" > <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> <div className="commandEntry-outerDiv" ref={this._viewRef} onPointerDown={this.dragViewDown}> @@ -337,7 +354,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.type)}> - {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( + {Object.values(CollectionViewType).filter(type => !excludedViewTypes.includes(type)).map(type => ( <option key={Utils.GenerateGuid()} className="collectionViewBaseChrome-viewOption" @@ -353,9 +370,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + return SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get notACollection() { @@ -366,6 +381,98 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } else return false; } + @computed + get pinButton() { + const targetDoc = this.selectedDoc; + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + <button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderLeft: "1px solid gray" }} + onClick={e => TabDocView.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + </button> + </Tooltip>; + } + + @computed + get pinWithViewButton() { + const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: "auto", width: 19 }} />; + const targetDoc = this.selectedDoc; + return (!targetDoc || (targetDoc._viewType !== CollectionViewType.Freeform && targetDoc.type !== DocumentType.IMG)) ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top"> + <button className="antimodeMenu-button" style={{ borderRight: "1px solid gray", borderLeft: "1px solid gray", justifyContent: 'center' }} + onClick={e => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } + }}> + {presPinWithViewIcon} + </button> + </Tooltip>; + } + + + @undoBatch + onAlias = () => { + if (this.selectedDoc && this.selectedDocumentView) { + // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); + // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + // copy.y = NumCast(this.selectedDoc.y) + 30; + // this.selectedDocumentView.props.addDocument?.(copy); + const alias = Doc.MakeAlias(this.selectedDoc); + alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + alias.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(alias); + } + } + onAliasButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + } + + @undoBatch + onAliasButtonMoved = (e: PointerEvent) => { + const contentDiv = this.selectedDocumentView?.ContentDiv; + if (contentDiv) { + const dragData = new DragManager.DocumentDragData([this.selectedDocumentView!.props.Document]); + const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; + dragData.defaultDropAction = "alias"; + dragData.canEmbed = true; + DragManager.StartDocumentDrag([contentDiv], dragData, e.clientX, e.clientY, { + offsetX: offset[0], + offsetY: offset[1], + hideSource: false + }); + return true; + } + return false; + } + + @computed + get aliasButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: "drag" }}> + <FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" /> + </button> + </Tooltip>; + } + + @computed get lightboxButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Lightbox of Images"}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={action(() => targetDoc._isLightboxOpen = true)} onClick={this.onAlias}> + <FontAwesomeIcon className="documentdecorations-icon" icon="desktop" size="lg" /> + </button> + </Tooltip>; + } + + render() { return ( @@ -374,15 +481,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <div className="collectionViewBaseChrome"> {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} {!this._buttonizableCommands ? (null) : this.templateChrome} - {Doc.UserDoc().noviceMode ? (null) : - <Tooltip title={<div className="dash-tooltip">filter documents to show</div>} placement="bottom"> - <div className="collectionViewBaseChrome-viewSpecs" style={{ display: "grid" }}> - <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > - <FontAwesomeIcon icon="filter" size="lg" /> - </button> - </div> - </Tooltip>} - {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> <button className={"antimodeMenu-button"} key="float" @@ -391,6 +489,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> </button> </Tooltip>} + {this.notACollection ? (null) : this.lightboxButton} + {this.aliasButton} + {/* {this.pinButton} */} + {this.pinWithViewButton} </div> {this.subChrome} </div> @@ -415,23 +517,21 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } get document() { return this.props.docView.props.Document; } @computed get dataField() { - return this.document[Doc.LayoutFieldKey(this.document)]; + return this.document[this.props.docView.LayoutFieldKey]; } @computed get childDocs() { return DocListCast(this.dataField); } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + return SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get isText() { if (this.selectedDoc) { const layoutField = Doc.LayoutField(this.selectedDoc); - return StrCast(layoutField).includes("FormattedText") || - (layoutField instanceof Doc && StrCast(layoutField.layout).includes("FormattedText")); + const layoutStr = this.selectedDocumentView?.props.LayoutTemplateString || StrCast(layoutField); + return (document.activeElement as any)?.className.includes("ProseMirror") || layoutStr.includes("FormattedText") || StrCast((layoutField as Doc)?.layout).includes("FormattedText"); } else return false; } @@ -439,30 +539,25 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @undoBatch @action nextKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); - this.document.currentFrame = Math.max(0, (currentFrame || 0) + 1); - this.document.lastFrame = Math.max(NumCast(this.document.currentFrame), NumCast(this.document.lastFrame)); + this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1); + this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame)); } @undoBatch @action prevKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.document.currentFrame = Math.max(0, (currentFrame || 0) - 1); - } - @undoBatch - @action - miniMap = (): void => { - this.document.hideMinimap = !this.document.hideMinimap; + this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1); } private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; @@ -584,10 +679,10 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu style={{ backgroundColor: this._colorBtn ? "121212" : "", zIndex: 1001 }}> {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} <div className="color-previewII" style={{ backgroundColor: color }}> - {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + {color === "" ? <p style={{ fontSize: 40, color: "red", marginTop: -10, marginLeft: -5, position: "fixed" }}>☒</p> : ""} </div> - </button>)} - </div>; + </button >)} + </div >; } @computed get fillPicker() { const fillPicker = this.toggleButton("shape fill color", this._fillBtn, () => this._fillBtn = !this._fillBtn, "fill-drip", @@ -600,53 +695,172 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} style={{ backgroundColor: this._fillBtn ? "121212" : "", zIndex: 1001 }}> <div className="color-previewII" style={{ backgroundColor: color }}> - {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + {color === "" ? <p style={{ fontSize: 40, color: "red", marginTop: -10, marginLeft: -5, position: "fixed" }}>☒</p> : ""} </div> </button>)} </div>; } + onUrlDrop = (e: React.DragEvent) => { + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const uri = dataTransfer.getData("text/uri-list"); + const url = uri || html || this._url; + const newurl = url.startsWith(window.location.origin) ? + url.replace(window.location.origin, this._url.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + this.submitURL(newurl); + e.stopPropagation(); + } + onUrlDragover = (e: React.DragEvent) => { + e.preventDefault(); + } + + @computed get _url() { + return this.selectedDoc?.data instanceof WebField ? Cast(this.selectedDoc.data, WebField, null)?.url.toString() : Field.toString(this.selectedDoc?.data as Field); + } + + set _url(value) { + if (this.selectedDoc) { + Doc.GetProto(this.selectedDoc).data = new WebField(value); + Doc.SetInPlace(this.selectedDoc, "title", value, true); + const annots = Doc.GetProto(this.selectedDoc)["data-annotations-" + this.urlHash(value)]; + Doc.GetProto(this.selectedDoc)["data-annotations"] = annots instanceof ObjectField ? ObjectField.MakeCopy(annots) : new List<Doc>([]); + } + } + + @action + submitURL = (url: string) => { + if (!url.startsWith("http")) url = "http://" + url; + try { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const URLy = new URL(url); + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + const annos = DocListCast(selectedDoc["data-annotations"]); + if (Field.toString(selectedDoc.data as Field) === Field.toString(new WebField(URLy))) { + Doc.GetProto(selectedDoc).data = new WebField(new URL("http://cs.brown.edu/~avd")); + setTimeout(action(() => Doc.GetProto(selectedDoc).data = new WebField(URLy)), 100); + } else { + if (url) { + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(annos); + if (history === undefined) { + selectedDoc["data-history"] = new List<string>([this._url]); + } else { + history.push(this._url); + } + future && (future.length = 0); + } + this._url = url; + } + } + } catch (e) { + console.log("WebBox URL error:" + url); + } + } + + urlHash(s: string) { + return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); + } + + onValueKeyDown = async (e: React.KeyboardEvent) => { + e.key === "Enter" && this.submitURL(this._keyInput.current!.value); + e.stopPropagation(); + } + + @action + forward = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (future?.length) { + history?.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = future.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + @action + back = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (history?.length) { + if (future === undefined) selectedDoc["data-future"] = new List<string>([this._url]); + else future.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = history.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + private _keyInput = React.createRef<HTMLInputElement>(); + + @computed get urlEditor() { + return ( + <div className="webBox-buttons" + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} style={{ display: "flex" }}> + <input className="webpage-urlInput" key={this._url} + placeholder="ENTER URL" + defaultValue={this._url} + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} + onKeyDown={this.onValueKeyDown} + onClick={(e) => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + ref={this._keyInput} + /> + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> + <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> + GO + </button> + <button className="submitUrl" onClick={this.back}> + <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> + </button> + <button className="submitUrl" onClick={this.forward}> + <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> + </button> + </div> + </div> + ); + } + + @observable viewType = this.selectedDoc?._viewType; render() { return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {this.props.docView.props.renderDepth !== 0 || this.isText || this.props.isDoc ? (null) : - <Tooltip key="map" title={<div className="dash-tooltip">Toggle Mini Map</div>} placement="bottom"> - <div className="backKeyframe" onClick={this.miniMap} style={{ marginRight: "5px" }}> - <FontAwesomeIcon icon={"map"} size={"lg"} /> - </div> - </Tooltip> - } - {!this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> <div className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> </Tooltip> : null} - {!this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> <div className="numKeyframe" style={{ color: this.document.editing ? "white" : "black", backgroundColor: this.document.editing ? "#5B9FDD" : "#AEDDF8" }} onClick={action(() => this.document.editing = !this.document.editing)} > - {NumCast(this.document.currentFrame)} + {NumCast(this.document._currentFrame)} </div> </Tooltip> : null} - {!this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> <div className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> </Tooltip> : null} {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : - <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> - <button className={"antimodeMenu-button"} key="hypothesis" - style={{ - backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, - borderRight: "1px solid gray" - }} - onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> - <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> - </button> - </Tooltip> + this.urlEditor } {!this.isText ? <> @@ -679,15 +893,14 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || - (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || - key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || - key.indexOf("lastModified") >= 0 || (key[0]?.toUpperCase() === key[0] && - key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || + key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || + (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } } @@ -935,16 +1148,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp get numCols() { return NumCast(this.document.gridNumCols, 10); } /** - * Sets the value of `numCols` on the grid's Document to the value entered. - */ - @undoBatch - onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter" || e.key === "Tab") { - if (e.currentTarget.valueAsNumber > 0) { - this.document.gridNumCols = e.currentTarget.valueAsNumber; - } - - } + * Sets the value of `numCols` on the grid's Document to the value entered. + */ + onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.currentTarget.valueAsNumber > 0) undoBatch(() => this.document.gridNumCols = e.currentTarget.valueAsNumber)(); } /** @@ -985,6 +1192,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; undoBatch(() => this.document.gridNumCols = this.numCols - 1)(); + if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; } @@ -1040,7 +1248,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp <span className="grid-icon"> <FontAwesomeIcon icon="columns" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> + <input className="collectionGridViewChrome-entryBox" type="number" value={this.numCols} onChange={this.onNumColsChange} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> <input className="collectionGridViewChrome-columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> <input className="collectionGridViewChrome-columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> </span> @@ -1083,4 +1291,15 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp </div> ); } -}
\ No newline at end of file +} +Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { + const dataField = doc[Doc.LayoutFieldKey(doc)]; + const childDocs = DocListCast(dataField); + const currentFrame = Cast(doc._currentFrame, "number", null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + doc._currentFrame = Math.max(0, newFrame); +}); diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 2e4055256..5b4730848 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,44 +1,38 @@ -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { HeightSym, Opt, WidthSym, Doc } from "../../../fields/Doc"; -import { ScriptField } from "../../../fields/ScriptField"; -import { BoolCast, NumCast, StrCast } from "../../../fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; +import { NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { UndoManager } from "../../util/UndoManager"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { CollectionSubView } from "./CollectionSubView"; import "./CollectionPileView.scss"; +import { CollectionSubView } from "./CollectionSubView"; import React = require("react"); -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { UndoManager, undoBatch } from "../../util/UndoManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { DragManager } from "../../util/DragManager"; -import { DocUtils } from "../../documents/Documents"; @observer export class CollectionPileView extends CollectionSubView(doc => doc) { - _lastTap = 0; - _doubleTap: boolean | undefined = false; _originalChrome: string = ""; - @observable _contentsActive = true; - @observable _collapsed: boolean = false; - @observable _childClickedScript: Opt<ScriptField>; + componentDidMount() { if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { this.Document._pileLayoutEngine = "pass"; } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); this.layoutDoc._chromeStatus = "disabled"; - this.layoutDoc.hideFilterView = true; } componentWillUnmount() { - this.layoutDoc.hideFilterView = false; this.layoutDoc._chromeStatus = this._originalChrome; } layoutEngine = () => StrCast(this.Document._pileLayoutEngine); + // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} > + const draggingSelf = this.props.isSelected(); + return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none", zIndex: this.layoutEngine() === "starburst" && !SnappingManager.GetIsDragging() ? -10 : "auto" }} > <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} addDocument={(doc: Doc | Doc[]) => { (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); @@ -50,6 +44,8 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { }} /> </div>; } + + // toggles the pileup between starburst to compact toggleStarburst = action(() => { if (this.layoutEngine() === 'starburst') { const defaultSize = 110; @@ -80,11 +76,11 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } }); + // for dragging documents out of the pileup view _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; SnappingManager.SetIsDragging(true); - // this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { if (this.layoutEngine() === "pass" && this.childDocs.length && e.shiftKey) { dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]); @@ -110,8 +106,9 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap } + // onClick for toggling the pileup view onClick = (e: React.MouseEvent) => { - if (e.button === 0) {//} && this._doubleTap) { + if (e.button === 0) { SelectionManager.DeselectAll(); this.toggleStarburst(); e.stopPropagation(); @@ -119,8 +116,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } render() { - - return <div className={"collectionPileView"} onClick={this.onClick} onPointerDown={this.pointerDown} + return <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> {this.contents} </div>; diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index c566be7de..2896a8243 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,44 +1,38 @@ import React = require("react"); -import { action, observable, trace, computed, runInAction } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter, Utils, emptyPath } from "../../../Utils"; +import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; -import { KeyCodes } from "../../util/KeyCodes"; -import { SetupDrag, DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; -import { Transform } from "../../util/Transform"; -import { MAX_ROW_HEIGHT, COLLECTION_BORDER_WIDTH } from '../globalCssVariables.scss'; -import '../DocumentDecorations.scss'; -import { EditableView } from "../EditableView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "./CollectionSchemaView.scss"; -import { CollectionView, Flyout } from "./CollectionView"; -import { NumCast, StrCast, BoolCast, FieldValue, Cast, DateCast } from "../../../fields/Types"; -import { Docs } from "../../documents/Documents"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faExpand } from '@fortawesome/free-solid-svg-icons'; +import { List } from "../../../fields/List"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; -import { SnappingManager } from "../../util/SnappingManager"; import { ComputedField } from "../../../fields/ScriptField"; +import { BoolCast, Cast, DateCast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; -import { List } from "../../../fields/List"; -import { OverlayView } from "../OverlayView"; -import { DocumentIconContainer } from "../nodes/DocumentIcon"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { DateField } from "../../../fields/DateField"; -import { RichTextField } from "../../../fields/RichTextField"; +import { Utils, emptyFunction } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; +import { KeyCodes } from "../../util/KeyCodes"; +import { CompileScript } from "../../util/Scripting"; import { SearchUtil } from "../../util/SearchUtil"; +import { SnappingManager } from "../../util/SnappingManager"; +import { undoBatch } from "../../util/UndoManager"; +import '../DocumentDecorations.scss'; +import { EditableView } from "../EditableView"; +import { MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; +import { DocumentIconContainer } from "../nodes/DocumentIcon"; +import { OverlayView } from "../OverlayView"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "./CollectionView"; const path = require('path'); -library.add(faExpand); - export interface CellProps { row: number; col: number; @@ -64,20 +58,25 @@ export interface CellProps { @observer export class CollectionSchemaCell extends React.Component<CellProps> { + public static resolvedFieldKey(column: string, rowDoc: Doc) { + const fieldKey = column; + if (fieldKey.startsWith("*")) { + const rootKey = fieldKey.substring(1); + const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))]; + const matchedKeys = allKeys.filter(key => key.includes(rootKey)); + if (matchedKeys.length) return matchedKeys[0]; + } + return fieldKey; + } @observable protected _isEditing: boolean = false; protected _focusRef = React.createRef<HTMLDivElement>(); - protected _document = this.props.rowProps.original; + protected _rowDoc = this.props.rowProps.original; + protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); protected _dropDisposer?: DragManager.DragDropDisposer; - - async componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - } - @observable contents: string = ""; - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } + componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } + componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } @action onKeyDown = (e: KeyboardEvent): void => { @@ -99,7 +98,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { - + this.onItemDown(e); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); @@ -113,61 +112,39 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } catch { } } - // this._isEditing = true; - // this.props.setIsEditing(true); - - 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); + const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null); + doc && this.props.setPreviewDoc(doc); } @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); if (!res.success) return false; - // doc[this.props.fieldKey] = res.result; - // return true; - doc[this.props.rowProps.column.id as string] = res.result; + doc[this.renderFieldKey] = res.result; return true; } private drop = (e: Event, de: DragManager.DropEvent) => { 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]; + this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; } else { const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); - this._document[fieldKey] = coll; + this._rowDataDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } } protected dropRef = (ele: HTMLElement | null) => { - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); - } + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } - // expandDoc = (e: React.PointerEvent) => { - // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; - // let doc = FieldValue(Cast(field, Doc)); - - // this.props.setPreviewDoc(doc!); - - // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - - // e.stopPropagation(); - // } - - returnHighlights(bing: (() => string), positions?: number[]) { - const results = []; - const contents = bing(); - - if (positions !== undefined) { + returnHighlights(contents: string, positions?: number[]) { + if (positions) { + const results = []; StrCast(this.props.Document._searchString); const length = StrCast(this.props.Document._searchString).length; const color = contents ? "black" : "grey"; @@ -182,71 +159,24 @@ export class CollectionSchemaCell extends React.Component<CellProps> { ); return results; } - else { - return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; - } + return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; } - type: string = ""; + @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); } + onItemDown = async (e: React.PointerEvent) => { + if (this.props.Document._searchDoc) { + const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); + const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext, + undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + } + } renderCellWithType(type: string | undefined) { const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - docFilters: returnEmptyFilter, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne - }; + const fieldKey = this.renderFieldKey; + const field = this._rowDoc[fieldKey]; - let matchedKeys = [props.fieldKey]; - if (props.fieldKey.startsWith("*")) { - const allKeys = Array.from(Object.keys(props.Document)); - allKeys.push(...Array.from(Object.keys(Doc.GetProto(props.Document)))); - matchedKeys = allKeys.filter(key => key.includes(props.fieldKey.substring(1))); - } - const fieldKey = matchedKeys.length ? matchedKeys[0] : props.fieldKey; - const field = props.Document[fieldKey]; - const doc = FieldValue(Cast(field, Doc)); - const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); - - const onItemDown = async (e: React.PointerEvent) => { - if (this.props.Document._searchDoc !== undefined) { - const doc = Doc.GetProto(this.props.rowProps.original); - const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); - let targetContext = undefined; - if (aliasdoc.length > 0) { - targetContext = Cast(aliasdoc[0].context, Doc) as Doc; - } - DocumentManager.Instance.jumpToDocument(this.props.rowProps.original, false, undefined, targetContext); - } - else { - fieldIsDoc && - SetupDrag(this._focusRef, - () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc | Doc[], target: Doc | undefined, addDoc: (newDoc: Doc | Doc[]) => any) => addDoc(doc) : this.props.moveDocument, - this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); - } - }; const onPointerEnter = (e: React.PointerEvent): void => { if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; @@ -256,49 +186,17 @@ export class CollectionSchemaCell extends React.Component<CellProps> { dragRef.current!.className = "collectionSchemaView-cellContainer"; }; - let contents: any = "incorrect type"; - if (type === undefined) contents = field === undefined ? undefined : Field.toString(field as Field);//StrCast(field) === "" ? "--" : <FieldView {...props} fieldKey={fieldKey} />; - if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--"; - if (type === "string") { - fieldKey === "text" ? - contents = Cast(field, RichTextField)?.Text : - 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") { - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "image") { - const image = FieldValue(Cast(field, ImageField)); - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "list") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "date") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - + let contents = Field.toString(field as Field); + contents = contents === "" ? "--" : contents; let className = "collectionSchemaView-cellWrapper"; if (this._isEditing) className += " editing"; if (this.props.isFocused && this.props.isEditable) className += " focused"; if (this.props.isFocused && !this.props.isEditable) className += " inactive"; - - // let docExpander = ( - // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} > - // <FontAwesomeIcon icon="expand" size="sm" /> - // </div> - // ); const positions = []; - let cfield = props.Document[props.fieldKey]; - this.type = props.fieldKey; if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { - let term = (cfield instanceof Promise) ? "...promise pending..." : Field.toString(cfield as Field); - term = term.toLowerCase(); + let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); let tally = 0; @@ -315,178 +213,95 @@ export class CollectionSchemaCell extends React.Component<CellProps> { positions.pop(); } } - let search = false; - if (this.props.Document._searchDoc !== undefined) { - search = true; - } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} + <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> - <div className={className} ref={this._focusRef} onPointerDown={onItemDown} tabIndex={-1}> - <div className="collectionSchemaView-cellContents" - ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}> - {!search ? + <div className={className} ref={this._focusRef} tabIndex={-1}> + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> + {!this.props.Document._searchDoc ? <EditableView - positions={positions.length > 0 ? positions : undefined} - search={Cast(this.props.Document._searchString, "string", null)} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents} - highlight={positions.length > 0 ? true : undefined} - //contents={StrCast(contents)} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} placeholder={placeholder} - bing={() => { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (cfield !== undefined) { - // if (typeof(cfield)===RichTextField) - const a = cfield as RichTextField; - const b = cfield as DateField; - console.log(b); - if (a.Text !== undefined) { - return (a.Text); - } - else if (b.toString() !== undefined) { - return b.toString(); - } - else if (StrCast(cfield)) { - return StrCast(cfield); - } - else { - return String(NumCast(cfield)); - } - } - }} GetValue={() => { - if (type === "number" && (contents === 0 || contents === "0")) { - return "0"; - } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (type === "number") { - return StrCast(cfield); - } - const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; - const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; - const val = cscript !== undefined ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : - Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; - return val; - - } - + const cfield = ComputedField.WithoutComputed(() => FieldValue(field)); + const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; + const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; + return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : + Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; }} SetValue={action((value: string) => { let retVal = false; - - if (value.startsWith(":=")) { - retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + if (value.startsWith(":=") || value.startsWith("=:=")) { + const script = value.substring(value.startsWith("=:=") ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); } else { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (script.compiled) { - retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - } - + const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const script = CompileScript(inputscript, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + script.compiled && (retVal = this.applyToDoc(inputscript.length !== value.length ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } if (retVal) { this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' this.props.setIsEditing(false); } return retVal; - - //return true; })} OnFillDown={async (value: string) => { 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) => value.startsWith(":=") ? - this.props.setComputed(value.substring(2), doc, this.props.rowProps.column.id!, i, this.props.col) : - this.applyToDoc(doc, i, this.props.col, script.run)); - } + script.compiled && DocListCast(this.props.Document[this.props.fieldKey]). + forEach((doc, i) => value.startsWith(":=") ? + this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : + this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)); }} /> : - this.returnHighlights(() => { - const dateCheck: Date | undefined = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : undefined; - if (dateCheck !== undefined) { - cfield = dateCheck.toLocaleString(); - } - if (props.fieldKey === "context") { - cfield = this.contents; - } - if (props.fieldKey === "*lastModified") { - if (FieldValue(props.Document["data-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["data-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - - else if (FieldValue(props.Document["text-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["text-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - } - return Field.toString(cfield as Field); - }, positions) + this.returnHighlights(contents, positions) } </div > - {/* {fieldIsDoc ? docExpander : null} */} </div> </div> ); } - render() { - return this.renderCellWithType(undefined); - } + render() { return this.renderCellWithType(undefined); } } @observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("number"); - } -} +export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } @observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("boolean"); - } -} +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } @observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("string"); - } -} +export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } @observer export class CollectionSchemaDateCell extends CollectionSchemaCell { - @observable private _date: Date = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : - this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof Date ? this.props.rowProps.original[this.props.rowProps.column.id as string] : new Date(); + @computed get _date(): Opt<DateField> { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; } @action handleChange = (date: any) => { - this._date = date; // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to - this._document[this.props.rowProps.column.id as string] = date as Date; + this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} } render() { - return <DatePicker - selected={this._date} - onSelect={date => this.handleChange(date)} - onChange={date => this.handleChange(date)} - />; + return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> : + <DatePicker + selected={this._date?.date || new Date} + onSelect={date => this.handleChange(date)} + onChange={date => this.handleChange(date)} + />; } } @@ -495,44 +310,11 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _doc = FieldValue(Cast(this._field, Doc)); - @observable private _docTitle = this._doc?.title; - @observable private _preview = false; - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableWidth() { return this.prop.PanelWidth() - 2 * this.borderWidth - 4 - this.previewWidth(); } + @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } @action onSetValue = (value: string) => { - this._docTitle = value; - //this.prop.Document[this.prop.fieldKey] = this._text; + this._doc && (Doc.GetProto(this._doc).title = value); const script = CompileScript(value, { addReturn: true, @@ -542,46 +324,22 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const results = script.compiled && script.run(); if (results && results.success) { - this._doc = results.result; - this._document[this.prop.fieldKey] = results.result; - this._docTitle = this._doc?.title; - + this._rowDoc[this.renderFieldKey] = results.result; return true; } return false; } + componentWillUnmount() { this.onBlur(); } + + onBlur = () => { this._overlayDisposer?.(); }; onFocus = () => { - this._overlayDisposer?.(); + this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } @action - onOpenClick = () => { - this._preview = false; - if (this._doc) { - this.props.addDocTab(this._doc, "onRight"); - return true; - } - return false; - } - - @action - showPreview = (bool: boolean, e: any) => { - if (this._isEditing) { - this._preview = false; - } else { - if (bool) { - this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY); - } else { - this.props.showDoc(undefined); - } - } - } - - @action - isEditingCalling = (isEditing: boolean): void => { - this.showPreview(false, ""); + isEditingCallback = (isEditing: boolean): void => { document.removeEventListener("keydown", this.onKeyDown); isEditing && document.addEventListener("keydown", this.onKeyDown); this._isEditing = isEditing; @@ -589,227 +347,90 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); } - onDown = (e: any) => { - this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - this.props.setPreviewDoc(this.props.rowProps.original); - - let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { - try { - new URL(url); - const temp = window.open(url)!; - temp.blur(); - window.focus(); - } catch { } - } - - 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); - - this.showPreview(true, e); - - } - render() { - if (typeof this._field === "object" && this._doc && this._docTitle) { - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} - onPointerDown={this.onDown} - onPointerEnter={(e) => { this.showPreview(true, e); }} - onPointerLeave={(e) => { this.showPreview(false, e); }} + return !this._doc ? this.renderCellWithType("document") : + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} + onPointerDown={this.onPointerDown} + > + <div className="collectionSchemaView-cellContents-document" + style={{ padding: "5.9px" }} + ref={this.dropRef} + onFocus={this.onFocus} + onBlur={this.onBlur} > - - <div className="collectionSchemaView-cellContents-document" - style={{ padding: "5.9px" }} - ref={this.dropRef} - onFocus={this.onFocus} - onBlur={() => this._overlayDisposer?.()} - > - - <EditableView - editing={this._isEditing} - isEditingCallback={this.isEditingCalling} - display={"inline"} - contents={this._docTitle} - height={"auto"} - maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return StrCast(this._docTitle); - }} - SetValue={action((value: string) => { - this.onSetValue(value); - this.showPreview(false, ""); - return true; - })} - /> - </div > - <div onClick={this.onOpenClick} className="collectionSchemaView-cellContents-docButton"> - <FontAwesomeIcon icon="external-link-alt" size="lg" ></FontAwesomeIcon> </div> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={"inline"} + contents={this._doc.title || "--"} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => StrCast(this._doc?.title)} + SetValue={action((value: string) => { + this.onSetValue(value); + return true; + })} + /> + </div > + <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + <FontAwesomeIcon icon="external-link-alt" size="lg" /> </div> - ); - } else { - return this.renderCellWithType("document"); - } + </div>; } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - // render() { - // return this.renderCellWithType("image"); - // } - - choosePath(url: URL, dataDoc: any) { - const lower = url.href.toLowerCase(); - if (url.protocol === "data") { - return url.href; - } else if (url.href.indexOf(window.location.origin) === -1) { - return Utils.CorsProxy(url.href); - } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) { - return url.href;//Why is this here - } + + choosePath(url: URL) { + if (url.protocol === "data") return url.href; + if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here + const ext = path.extname(url.href); - const _curSuffix = "_o"; - return url.href.replace(ext, _curSuffix + ext); + return url.href.replace(ext, "_o" + path.extname(url.href)); } render() { - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; + const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc + const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; + const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - let image = true; - let url = []; - if (props.DataDoc) { - const field = Cast(props.DataDoc[props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(props.DataDoc[props.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url, props.DataDoc)); // access the primary layout data of the alternate documents - const paths = field ? [this.choosePath(field.url, props.DataDoc), ...altpaths] : altpaths; - if (paths.length) { - url = paths; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } - //url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } + const heightToWidth = NumCast(this._rowDoc._nativeHeight) / NumCast(this._rowDoc._nativeWidth); + let width = Math.min(75, this.props.rowProps.width); + const height = Math.min(75, width * heightToWidth); + width = height / heightToWidth; - const heightToWidth = NumCast(props.DataDoc?._nativeHeight) / NumCast(props.DataDoc?._nativeWidth); - const height = this.props.rowProps.width * heightToWidth; - - if (props.fieldKey === "data") { - if (url !== []) { - const reference = React.createRef<HTMLDivElement>(); - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> - <img src={url[0]} width={image ? this.props.rowProps.width : "30px"} - height={image ? height : "30px"} /> - </div > - </div> - ); - - } else { - return this.renderCellWithType("image"); - } - } else { - return this.renderCellWithType("image"); - } + const reference = React.createRef<HTMLDivElement>(); + return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> + <img src={url[0]} + width={paths.length ? width : "20px"} + height={paths.length ? height : "20px"} /> + </div > + </div>; } } - - - @observer export class CollectionSchemaListCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _optionsList = this._field as List<any>; + @computed get _field() { return this._rowDoc[this.renderFieldKey]; } + @computed get _optionsList() { return this._field as List<any>; } @observable private _opened = false; @observable private _text = "select an item"; @observable private _selectedNum = 0; @action - toggleOpened(open: boolean) { - this._opened = open; - } - - // @action - // onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - // this._text = e.target.value; - - // // change if its a document - // this._optionsList[this._selectedNum] = this._text; - // } - - @action onSetValue = (value: string) => { - - - this._text = value; - // change if its a document - this._optionsList[this._selectedNum] = this._text; - - (this.prop.Document[this.prop.fieldKey] as List<any>).splice(this._selectedNum, 1, value); + this._optionsList[this._selectedNum] = this._text = value; + (this._field as List<any>).splice(this._selectedNum, 1, value); } @action @@ -823,55 +444,23 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } - render() { - - const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - - let type = "list"; - - let link = false; - let doc = false; + const link = false; const reference = React.createRef<HTMLDivElement>(); - if (typeof this._field === "object" && this._optionsList[0]) { - - const options = this._optionsList.map((element, index) => { - - if (element instanceof Doc) { - doc = true; - type = "document"; - if (this.prop.fieldKey.toLowerCase() === "links") { - link = true; - type = "link"; - } - const document = FieldValue(Cast(element, Doc)); - const title = element.title; - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element.title), index); }} - style={{ padding: "6px" }}> - {title} - </div>; - - } else { - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element), index); }} - style={{ padding: "6px" }}>{element}</div>; - } - }); + if (this._optionsList?.length) { + const options = !this._opened ? (null) : + <div> + {this._optionsList.map((element, index) => { + const val = Field.toString(element); + return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} > + {val} + </div>; + })} + </div>; const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>; - // const textarea = <textarea onChange={this.onChange} value={this._text} - // onFocus={doc ? this.onFocus : undefined} - // onBlur={doc ? e => this._overlayDisposer?.() : undefined} - // style={{ resize: "none" }} - // placeholder={"select an item"}></textarea>; - - const textarea = <div className="collectionSchemaView-cellContents" - style={{ padding: "5.9px" }} - ref={type === undefined || type === "document" ? this.dropRef : null} key={this.prop.Document[Id]}> + const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} > <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} @@ -879,11 +468,8 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { contents={this._text} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return this._text; - }} + GetValue={() => this._text} SetValue={action((value: string) => { - // add special for params this.onSetValue(value); return true; @@ -892,61 +478,35 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { </div >; //☰ - - const dropdown = <div> - {options} - </div>; - return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> <div className="collectionSchemaView-dropDownWrapper"> - <button type="button" className="collectionSchemaView-dropdownButton" onClick={(e) => { this.toggleOpened(!this._opened); }} - style={{ right: "length", position: "relative" }}> - {this._opened ? <FontAwesomeIcon icon="caret-up" size="lg" ></FontAwesomeIcon> - : <FontAwesomeIcon icon="caret-down" size="lg" ></FontAwesomeIcon>} + <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }} + onClick={action(e => this._opened = !this._opened)} > + <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" /> </button> <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div> </div> - - {this._opened ? dropdown : null} + {options} </div > </div> ); - } else { - return this.renderCellWithType("list"); } + return this.renderCellWithType("list"); } } - - - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false; - - @action - toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { - this._isChecked = e.target.checked; - 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); - } - } + @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } render() { 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)); - }; return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}> - <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} /> - </div > + <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} /> </div> ); } @@ -955,62 +515,15 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { @observer export class CollectionSchemaButtons extends CollectionSchemaCell { - render() { - // 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)); - // }; - const doc = this.props.rowProps.original; - let buttons: JSX.Element | undefined = undefined; - buttons = <div style={{ - paddingTop: 8, - paddingLeft: 3, - }}><button onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }} style={{ padding: 2, left: 77 }}> - <FontAwesomeIcon icon="arrow-up" size="sm" /> - </button> - <button onClick={() => { - { - doc.searchMatchAlt = false; - setTimeout(() => doc.searchMatchAlt = true, 0); - } - }} style={{ padding: 2 }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div>; - const type = StrCast(doc.type); - if (type === "pdf") { - buttons = <div><button - style={{ - position: "relative", - height: 30, - width: 28, - left: 1, - }} - - onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div >; - } - else if (type !== "rtf") { - buttons = undefined; - } - - if (BoolCast(this.props.Document._searchDoc) === true) { - - } - else { - buttons = undefined; - } - return ( - <div> {buttons}</div> - ); + return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> : + <div style={{ paddingTop: 8, paddingLeft: 3 }} > + <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}> + <FontAwesomeIcon icon="arrow-up" size="sm" /> + </button> + <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} > + <FontAwesomeIcon icon="arrow-down" size="sm" /> + </button> + </div>; } -} - +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index be25bf9de..dbf7488ec 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -13,57 +13,11 @@ import { SearchBox } from "../search/SearchBox"; import { ColumnType } from "./CollectionSchemaView"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; -import * as fa from '@fortawesome/free-solid-svg-icons'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -export interface HeaderProps { - keyValue: SchemaHeaderField; - possibleKeys: string[]; - existingKeys: string[]; - keyType: ColumnType; - typeConst: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; - setIsEditing: (isEditing: boolean) => void; - deleteColumn: (column: string) => void; - setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; - setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; - setColumnColor: (column: SchemaHeaderField, color: string) => void; - -} - -export class CollectionSchemaHeader extends React.Component<HeaderProps> { - render() { - 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 ? "sort-amount-down" : - this.props.keyType === ColumnType.Image ? "image" : this.props.keyType === ColumnType.List ? "list-ul" : this.props.keyType === ColumnType.Date ? "calendar" : - "align-justify"; - return ( - <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}> - <CollectionSchemaColumnMenu - columnField={this.props.keyValue} - // keyValue={this.props.keyValue.heading} - possibleKeys={this.props.possibleKeys} - existingKeys={this.props.existingKeys} - // keyType={this.props.keyType} - typeConst={this.props.typeConst} - menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>} - addNew={false} - onSelect={this.props.onSelect} - setIsEditing={this.props.setIsEditing} - deleteColumn={this.props.deleteColumn} - onlyShowOptions={false} - setColumnType={this.props.setColumnType} - setColumnSort={this.props.setColumnSort} - setColumnColor={this.props.setColumnColor} - /> - </div> - ); - } -} - export interface AddColumnHeaderProps { createColumn: () => void; @@ -79,7 +33,6 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe } - export interface ColumnMenuProps { columnField: SchemaHeaderField; // keyValue: string; @@ -103,37 +56,29 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> @observable private _isOpen: boolean = false; @observable private _node: HTMLDivElement | null = null; - componentDidMount() { - document.addEventListener("pointerdown", this.detectClick); - } + componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } - componentWillUnmount() { - document.removeEventListener("pointerdown", this.detectClick); - } + componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } - detectClick = (e: PointerEvent): void => { - if (this._node && this._node.contains(e.target as Node)) { - } else { - this._isOpen = false; - this.props.setIsEditing(false); - } + @action + detectClick = (e: PointerEvent) => { + !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false); } @action toggleIsOpen = (): void => { - this._isOpen = !this._isOpen; - this.props.setIsEditing(this._isOpen); + this.props.setIsEditing(this._isOpen = !this._isOpen); } - changeColumnType = (type: ColumnType): void => { + changeColumnType = (type: ColumnType) => { this.props.setColumnType(this.props.columnField, type); } - changeColumnSort = (desc: boolean | undefined): void => { + changeColumnSort = (desc: boolean | undefined) => { this.props.setColumnSort(this.props.columnField, desc); } - changeColumnColor = (color: string): void => { + changeColumnColor = (color: string) => { this.props.setColumnColor(this.props.columnField, color); } @@ -145,7 +90,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } renderTypes = () => { - if (this.props.typeConst) return <></>; + if (this.props.typeConst) return (null); const type = this.props.columnField.type; return ( @@ -291,7 +236,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable private _key: string = this.props.keyValue; @observable private _searchTerm: string = this.props.keyValue; @observable private _isOpen: boolean = false; - @observable private _canClose: boolean = true; + @observable private _node: HTMLDivElement | null = null; @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; @@ -306,6 +251,31 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(false); } + @action + setNode = (node: HTMLDivElement): void => { + if (node) { + this._node = node; + } + } + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters?.includes(this._key)) { + runInAction(() => this.closeResultsVisibility = "contents"); + } + } + + @action + detectClick = (e: PointerEvent): void => { + if (this._node && this._node.contains(e.target as Node)) { + } else { + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + private tempfilter: string = ""; @undoBatch onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { @@ -313,23 +283,24 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (temp === "") { - Doc.setDocFilter(this.props.Document, this._key, temp, undefined); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); } else { - Doc.setDocFilter(this.props.Document, this._key, temp, "match"); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.tempfilter = temp; + Doc.setDocFilter(this.props.Document, this._key, temp, "check"); this.props.col.setColor("green"); + this.closeResultsVisibility = "contents"; } } else { - let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const blockedkeys = ["_scrollTop", "customTitle", "limitHeight", "proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); - if (keyOptions.length) { - this.onSelect(keyOptions[0]); - console.log("case1"); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); + if (this.showKeys.length) { + this.onSelect(this.showKeys[0]); } else if (this._searchTerm !== "" && this.props.canAddNew) { this.setSearchTerm(this._searchTerm || this._key); - console.log("case2"); this.onSelect(this._searchTerm); } } @@ -346,22 +317,14 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(true); } - @action - onBlur = (e: React.FocusEvent): void => { - if (this._canClose) { - this._isOpen = false; - this.props.setIsEditing(false); - } - } - - @action - onPointerEnter = (e: React.PointerEvent): void => { - this._canClose = false; - } - - @action - onPointerOut = (e: React.PointerEvent): void => { - this._canClose = true; + @computed get showKeys() { + const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"]; + const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + const showKeys = new Set<string>(); + [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.UserDoc().noviceMode || + whitelistKeys.includes(key) + || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); + return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); } @action renderOptions = (): JSX.Element[] | JSX.Element => { @@ -369,28 +332,24 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.defaultMenuHeight = 0; return <></>; } - const searchTerm = this._searchTerm.trim() === "New field" ? "" : this._searchTerm; - - let keyOptions = 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; - - const blockedkeys = ["proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); - - const options = keyOptions.map(key => { + const options = this.showKeys.map(key => { return <div key={key} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} - onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + onPointerDown={e => { + e.stopPropagation(); + }} + onClick={() => { + this.onSelect(key); + this.setSearchTerm(""); + }}>{key}</div>; }); // if search term does not already exist as a group type, give option to create new group type if (this._key !== this._searchTerm.slice(0, this._key.length)) { - console.log("little further"); - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + if (this._searchTerm !== "" && this.props.canAddNew) { options.push(<div key={""} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} @@ -418,55 +377,55 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @action renderFilterOptions = (): JSX.Element[] | JSX.Element => { - if (!this._isOpen) { + if (!this._isOpen || !this.props.dataDoc) { this.defaultMenuHeight = 0; return <></>; } - const keyOptions: string[] = []; const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (this.docSafe.length === 0) { - this.docSafe = DocListCast(this.props.dataDoc![this.props.fieldKey]); + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); } const docs = this.docSafe; docs.forEach((doc) => { const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false && key.includes(temp)) { + if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { keyOptions.push(key); } }); const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) { if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) { keyOptions.push(filters![i + 1]); } } - - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { - this.props.col.setColor("rgb(241, 239, 235)"); - } - const options = keyOptions.map(key => { - //Doc.setDocFilter(this.props.Document!, this._key, key, undefined); let bool = false; - console.log(filters); if (filters !== undefined) { bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check"; - console.log(filters.includes(key)); } return <div key={key} className="key-option" style={{ border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", }} > - <input type="checkbox" onChange={(e) => { - e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); - e.target.checked === true ? this.props.col.setColor("green") : ""; - e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); - }} - checked={bool} ></input> + <input type="checkbox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + onChange={(e) => { + e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); + e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); + e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); + e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); + }} + checked={bool} + /> <span style={{ paddingLeft: 4 }}> {key} </span> @@ -492,6 +451,14 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable defaultMenuHeight = 0; + updateFilter() { + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } + } + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } @@ -501,29 +468,48 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { return script ? () => script : undefined; } filterBackground = () => "rgba(105, 105, 105, 0.432)"; - @observable filterOpen: boolean | undefined = undefined; + closeResultsVisibility: string = "none"; + + removeFilters = (e: React.PointerEvent): void => { + const keyOptions: string[] = []; + if (this.docSafe.length === 0 && this.props.dataDoc) { + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); + } + const docs = this.docSafe; + docs.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false) { + keyOptions.push(key); + } + }); + + Doc.setDocFilter(this.props.Document, this._key, "", "remove"); + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } render() { return ( - <div style={{ display: "flex" }}> + <div style={{ display: "flex" }} ref={this.setNode}> <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) }} /> */} - <div className="keys-dropdown" style={{ zIndex: 10, width: this.props.width, maxWidth: this.props.width }}> + <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> <input className="keys-search" style={{ width: "100%" }} ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} - onClick={(e) => { - //this._inputRef.current!.select(); - e.stopPropagation(); - }} onFocus={this.onFocus} onBlur={this.onBlur}></input> + onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} + onFocus={this.onFocus} ></input> + <div style={{ display: this.closeResultsVisibility }}> + <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg" + style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} /> + </div> {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ width: this.props.width, maxWidth: this.props.width, height: "auto", - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> + }}> {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} </div>} </div > diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 37e6c115d..881246bd4 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -1,21 +1,17 @@ import React = require("react"); -import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table"; -import "./CollectionSchemaView.scss"; -import { Transform } from "../../util/Transform"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; import { Doc } from "../../../fields/Doc"; -import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, FieldValue, StrCast } from "../../../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 { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; +import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SnappingManager } from "../../util/SnappingManager"; - -library.add(faGripVertical, faTrash); +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import "./CollectionSchemaView.scss"; export interface MovableColumnProps { columnRenderer: TableCellRenderer; @@ -205,6 +201,7 @@ export class MovableRow extends React.Component<MovableRowProps> { } onRowContextMenu = (e: React.MouseEvent): void => { + e.preventDefault(); 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" }); } @@ -226,13 +223,15 @@ export class MovableRow extends React.Component<MovableRowProps> { render() { const { children = null, rowInfo } = this.props; + if (!rowInfo) { return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>; } const { original } = rowInfo; const doc = FieldValue(Cast(original, Doc)); - if (!doc) return <></>; + + if (!doc) return (null); const reference = React.createRef<HTMLDivElement>(); const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); @@ -245,11 +244,11 @@ export class MovableRow extends React.Component<MovableRowProps> { <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > - {/* <div className="row-dragger"> - <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> - <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> - <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "onRight")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> - </div> */} + <div className="row-dragger"> + <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> + </div> {children} </ReactTableDefaults.TrComponent> </div> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index c1918aed0..8d2f645d9 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -59,6 +59,7 @@ justify-content: space-between; flex-wrap: nowrap; touch-action: none; + padding: 2px; div { touch-action: none; @@ -99,6 +100,9 @@ direction: ltr; overflow: visible; } + .rt-noData { + display: none; + } .rt-thead { width: 100%; @@ -139,6 +143,10 @@ width: 100%; direction: rtl; overflow: visible; + + .rt-td { + border-right: 1px solid rgba(0, 0, 0, 0.2); + } } .rt-tr-group { @@ -158,6 +166,8 @@ font-size: 13px; text-align: center; white-space: nowrap; + display: flex; + align-items: center; .imageBox-cont { position: relative; @@ -176,6 +186,11 @@ height: 100%; } } + .rt-td.rt-expandable { + display: flex; + align-items: center; + height: inherit; + } .rt-resizer { width: 8px; @@ -359,47 +374,6 @@ button.add-column { } } -.altcollectionTimeView-treeView { - display: flex; - flex-direction: column; - width: 175px; - height: auto; - position: fixed; - border-left: solid 1px; - z-index: 1; - - .collectionTimeView-addfacet { - display: inline-block; - width: 200px; - height: 30px; - background: darkGray; - text-align: left; - - .collectionTimeView-button { - align-items: center; - display: flex; - width: 100%; - height: 100%; - - .collectionTimeView-span { - margin: auto; - } - } - - >div, - >div>div { - width: 100%; - height: 100%; - } - } - - .altcollectionTimeView-tree { - display: inline-block; - width: 100%; - height: calc(100% - 30px); - } -} - .collectionSchema-row { height: 100%; background-color: white; @@ -417,11 +391,12 @@ button.add-column { .row-dragger { display: flex; justify-content: space-around; - flex: 50 0 auto; - width: 50px; + //flex: 50 0 auto; + width: 0; max-width: 50px; - height: 100%; + //height: 100%; min-height: 30px; + align-items: center; color: lightgray; background-color: white; transition: color 0.1s ease; @@ -429,10 +404,12 @@ button.add-column { .row-option { // padding: 5px; cursor: pointer; + position: absolute; transition: color 0.1s ease; display: flex; flex-direction: column; justify-content: center; + z-index: 2; &:hover { color: gray; @@ -462,7 +439,7 @@ button.add-column { .collectionSchemaView-cellContainer { width: 100%; - height: 100%; + height: unset; } .collectionSchemaView-cellWrapper { @@ -599,10 +576,17 @@ button.add-column { .collectionSchemaView-table { width: 100%; height: 100%; + overflow: auto; + padding: 3px; } +.rt-td.rt-expandable { + overflow: visible; + position: relative; + height:100%; + z-index: 1; +} .reactTable-sub { - padding: 10px 30px; background-color: rgb(252, 252, 252); width: 100%; @@ -610,12 +594,6 @@ button.add-column { display: none; } - .collectionSchemaView-table { - border: solid 1px; - overflow: hidden; - } - - .row-dragger { background-color: rgb(252, 252, 252); } @@ -625,20 +603,26 @@ button.add-column { } .collectionSchemaView-table { - width: 100%; + width: 100%; + border: solid 1px; + overflow: visible; + padding: 0px; } } .collectionSchemaView-expander { height: 100%; min-height: 30px; - position: relative; + position: absolute; color: gray; + width: 20; + height: auto; + left: 55; svg { position: absolute; top: 50%; - left: 50%; + left: 10; transform: translate(-50%, -50%); } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index a72b349ec..27575374a 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,32 +1,29 @@ import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Resize } from "react-table"; import "react-table/react-table.css"; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { Cast, NumCast, BoolCast } from "../../../fields/Types"; +import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { SchemaTable } from "./SchemaTable"; - -library.add(faCog, faPlus, faSortUp, faSortDown); -library.add(faTable); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -44,7 +41,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -71,7 +68,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get menuCoordinates() { let searchx = 0; let searchy = 0; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { const el = document.getElementsByClassName("collectionSchemaView-searchContainer")[0]; if (el !== undefined) { const rect = el.getBoundingClientRect(); @@ -303,17 +300,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; if (filter) { Doc.setDocFilter(this.props.Document, newKey, filter, "match"); - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - Doc.setDocFilter(doc, newKey, filter, "match"); - } } else { this.props.Document._docFilters = undefined; - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - doc._docFilters = undefined; - } } } } @@ -388,7 +377,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setFocused = (doc: Doc) => this._focusedTable = doc; - @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; + @action setPreviewDoc = (doc: Opt<Doc>) => { + SelectionManager.SelectSchemaDoc(this, doc); + this.previewDoc = doc; + } //toggles preview side-panel of schema @action @@ -437,8 +429,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <ContentFittingDocumentView Document={this.previewDocument} DataDoc={undefined} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={true} FreezeDimensions={true} focus={emptyFunction} @@ -449,6 +439,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} @@ -493,8 +485,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { documentKeys={this.documentKeys} headerIsEditing={this._headerIsEditing} openHeader={this.openHeader} - onClick={e => { e.stopPropagation(); this.closeHeader(); }} - onPointerDown={this.onTablePointerDown} + onClick={this.onTableClick} + onPointerDown={emptyFunction} onResizedChange={this.onResizedChange} setColumns={this.setColumns} reorderColumns={this.reorderColumns} @@ -515,14 +507,28 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> </div>; } + onSpecificMenu = (e: React.MouseEvent) => { + if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { + const cm = ContextMenu.Instance; + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + optionItems.push({ description: "remove", event: () => this.previewDoc && this.props.removeDocument(this.previewDoc), icon: "trash" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + cm.displayMenu(e.clientX, e.clientY); + (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. + e.stopPropagation(); + } + } @action - onTablePointerDown = (e: React.PointerEvent): void => { - this.setFocused(this.props.Document); - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { + onTableClick = (e: React.MouseEvent): void => { + if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { + this.setPreviewDoc(undefined); + } else { e.stopPropagation(); } - // this.closeHeader(); + this.setFocused(this.props.Document); + this.closeHeader(); } onResizedChange = (newResized: Resize[], event: any) => { @@ -572,7 +578,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { let name = "collectionSchemaView-container"; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { name = "collectionSchemaView-searchContainer"; } if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); @@ -594,13 +600,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div>; return <div className={name} style={{ - overflow: this.props.overflow === true ? "scroll" : undefined, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, + overflow: this.props.overflow === true ? "scroll" : undefined, backgroundColor: "white", + pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", }} > <div className="collectionSchemaView-tableContainer" - style={{ backgroundColor: "white", width: `calc(100% - ${this.previewWidth()}px)` }} + style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onKeyPress={this.onKeyPress} + onContextMenu={this.onSpecificMenu} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 8fc74a9c6..9f56a0c0e 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -205,7 +205,7 @@ display: flex; align-items: center; justify-content: center; - color: lightGray; + color: black; .editableView-container-editing-oneLine, .editableView-container-editing { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 241c64f9a..daaee67dc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -12,7 +12,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, smoothScroll } from "../../../Utils"; +import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, smoothScroll, returnVal } from "../../../Utils"; import { DragManager, dropActionType } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -33,25 +33,31 @@ const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; const StackingDocument = makeInterface(collectionSchema, documentSchema); +export type collectionStackingViewProps = { + chromeStatus?: string; +}; + @observer -export class CollectionStackingView extends CollectionSubView(StackingDocument) { +export class CollectionStackingView extends CollectionSubView<StackingDocument, Partial<collectionStackingViewProps>>(StackingDocument) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); _pivotFieldDisposer?: IReactionDisposer; + _autoHeightDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @observable _scroll = 0; // used to force the document decoration to update when scrolling + @computed get chromeStatus() { return this.props.chromeStatus || StrCast(this.layoutDoc._chromeStatus); } @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField)); } @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, this.props.yMargin || NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.layoutDoc._columnsStack, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.pivotField && (this.layoutDoc._chromeStatus !== 'view-mode' && this.layoutDoc._chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / this.props.ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); @@ -78,7 +84,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)} + {this.getDisplayDoc(d, dxf, width)} </div>; }); } @@ -127,19 +133,6 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return fields; } - getSimpleDocHeight(d?: Doc) { - if (!d) return 0; - const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); - let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - if (!layoutDoc._fitWidth && nw && nh) { - const aspect = nw && nh ? nh / nw : 1; - if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); - return wid * aspect; - } - return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym](); - } componentDidMount() { super.componentDidMount?.(); @@ -148,10 +141,12 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) () => this.pivotField, () => this.layoutDoc._columnHeaders = new List() ); + this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, this.forceAutoHeight); } componentWillUnmount() { super.componentWillUnmount(); this._pivotFieldDisposer?.(); + this._autoHeightDisposer?.(); } @action @@ -186,17 +181,16 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } - afterFocus && setTimeout(() => { - if (afterFocus?.()) { } - }, 500); + afterFocus && setTimeout(afterFocus, 500); } - getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { + getDisplayDoc(doc: Doc, dxf: () => Transform, width: () => number) { + const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); - const opacity = () => this.Document.currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document.currentFrame))?.opacity; + const opacity = () => this.Document._currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity; return <ContentFittingDocumentView Document={doc} - DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])} + DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} backgroundColor={this.props.backgroundColor} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -205,10 +199,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : undefined} + dontCenter={this.props.childIgnoreNativeSize ? true : false} fitToBox={false} - dontRegisterView={BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)} + dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} @@ -217,11 +212,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) opacity={opacity} focus={this.focusDocument} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} + contentsPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.addDocTab} @@ -240,17 +238,22 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } getDocHeight(d?: Doc) { if (!d) return 0; + const dataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); + const layoutField = Doc.LayoutFieldKey(layoutDoc); + const nw = NumCast(layoutDoc._nativeWidth) || NumCast(dataDoc?.[`${layoutField}-nativeWidth`]); + const nh = NumCast(layoutDoc._nativeHeight) || NumCast(dataDoc?.[`${layoutField}-nativeHeight`]); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); + const hllimit = NumCast(this.layoutDoc.childLimitHeight, -1); if (!layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); - return wid * aspect; + if (!(this.layoutDoc._columnsFill)) wid = Math.min(this.getDocWidth(d), wid); + return Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, wid * aspect); } - return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]()); + return layoutDoc._fitWidth ? + (!nh ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * nh / (nw || 1), this.layoutDoc._autoHeight ? 100000 : this.props.PanelHeight() - 2 * this.yMargin)) : + Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, Math.max(20, layoutDoc[HeightSym]())); } columnDividerDown = (e: React.PointerEvent) => { @@ -340,9 +343,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); - if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - type = types[0]; + if (this.pivotField) { + 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]; + } } 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)))); @@ -354,16 +359,17 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + const height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); } }} - key={heading ? heading.heading : ""} + key={heading?.heading ?? ""} cols={cols} headings={this.headings} - heading={heading ? heading.heading : ""} + heading={heading?.heading ?? ""} headingObject={heading} docList={docList} parent={this} @@ -383,6 +389,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + offsety); } + forceAutoHeight = () => { + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + } + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; @@ -401,7 +412,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); @@ -439,16 +451,13 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } - onToggle = (checked: Boolean) => { - this.layoutDoc._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()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._columnsFill ? "Variable Size" : "Autosize"} Column`, event: () => this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill, icon: "plus" }); subItems.push({ description: `${this.layoutDoc._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + subItems.push({ description: "Clear All", event: () => this.dataDoc.data = new List([]), icon: "times" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } @@ -464,8 +473,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } - @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth) || this.props.NativeWidth() || 0; } - @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight) || this.props.NativeHeight() || 0; } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc._nativeWidth)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc._nativeHeight)); } @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } @@ -488,10 +497,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) width: `${1 / this.scaling * 100}%`, transformOrigin: "top left", }} - onScroll={action(e => { - if (!this.props.isSelected() && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; - else this._scroll = e.currentTarget.scrollTop; - })} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this.props.active(true) && e.stopPropagation()} > @@ -501,13 +507,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.layoutDoc._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={this.layoutDoc._chromeStatus !== 'view-mode'} - checkedChildren="edit" - unCheckedChildren="view" - /> : null} + {/* {this.chromeStatus === 'disabled' || !this.props.isSelected() ? (null) : + <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={this.chromeStatus !== 'view-mode'} + checkedChildren="edit" + unCheckedChildren="view" + />} */} </div> </div> ); } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index ede75fba8..1bc989e83 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,35 +1,33 @@ 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, runInAction, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; -import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; -import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; -import { listSpec } from "../../../fields/Schema"; -import { SnappingManager } from "../../util/SnappingManager"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; +import { Id } from "../../../fields/FieldSymbols"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faPalette); - interface CSVFieldColumnProps { cols: () => number; headings: () => object[]; @@ -141,8 +139,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC 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; - this.props.parent.props.addDocument(newDoc); - return false; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = " "; + return this.props.parent.props.addDocument(newDoc); } @action @@ -232,7 +231,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const docItems: ContextMenuProps[] = []; const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; - DocUtils.addDocumentCreatorMenuItems(this.props.parent.props.addDocument, this.props.parent.props.addDocument, x, y); + DocUtils.addDocumentCreatorMenuItems((doc) => { + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.parent.props.addDocument(doc); + }, this.props.parent.props.addDocument, x, y, true); Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => docItems.push({ @@ -260,13 +262,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } }, icon: "compress-arrows-alt" })); - layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200 })), 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" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); ContextMenu.Instance.setDefaultItem("::", (name: string): void => { Doc.GetProto(this.props.parent.props.Document)[name] = ""; const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); @@ -289,9 +286,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; - const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin, 5); + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.yMargin, 5); 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 evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { GetValue: () => evContents, SetValue: this.headingChanged, @@ -310,10 +307,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ - marginTop: NumCast(this.props.parent.props.Document._yMargin, 5), + marginTop: NumCast(this.props.parent.yMargin, 5), 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.chromeStatus !== 'view-mode' && this.props.parent.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. @@ -346,7 +343,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - const chromeStatus = this.props.parent.props.Document._chromeStatus; + const chromeStatus = this.props.parent.chromeStatus; const type = this.props.parent.props.Document.type; return <> {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} @@ -369,7 +366,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 70 }}> + style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 10 }}> <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 075be41fd..649b8c175 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,8 +1,7 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; -import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; +import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; @@ -31,8 +30,8 @@ export interface CollectionViewProps extends FieldViewProps { setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; rootSelected: (outsideReaction?: boolean) => boolean; fieldKey: string; - NativeWidth: () => number; - NativeHeight: () => number; + NativeWidth?: () => number; + NativeHeight?: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { @@ -40,6 +39,7 @@ export interface SubCollectionViewProps extends CollectionViewProps { children?: never | (() => JSX.Element[]) | React.ReactNode; ChildLayoutTemplate?: () => Doc; childOpacity?: () => number; + childIgnoreNativeSize?: boolean; ChildLayoutString?: string; childClickScript?: ScriptField; childDoubleClickScript?: ScriptField; @@ -111,6 +111,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return this.props.ignoreFields?.includes("_docFilters") ? [] : [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } + docRangeFilters = () => { + return this.props.ignoreFields?.includes("_docRangeFilters") ? [] : + [...this.props.docRangeFilters(), ...Cast(this.props.Document._docRangeFilters, listSpec("string"), [])]; + } + searchFilterDocs = () => { + return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)]; + } @computed get childDocs() { let rawdocs: (Doc | Promise<Doc>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; @@ -128,52 +135,38 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - let searchDocs = DocListCast(this.props.Document._searchDocs); - - - let docsforFilter: Doc[] = childDocs; + const docFilters = this.docFilters(); + const docRangeFilters = this.docRangeFilters(); + const searchDocs = this.searchFilterDocs(); + if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) return childDocs; - if (searchDocs !== undefined && searchDocs.length > 0) { - docsforFilter = []; - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - console.log(searchDocs); - searchDocs = DocUtils.FilterDocs(searchDocs, this.docFilters(), docRangeFilters, viewSpecScript); - console.log(this.docFilters()); - console.log(searchDocs); - childDocs.forEach((d) => { - if (d.data !== undefined) { - let newdocs = DocListCast(d.data); - if (newdocs.length > 0) { - let displaycheck: boolean | undefined = undefined; - let newarray: Doc[] = []; - while (newdocs.length > 0) { - newarray = []; - newdocs.forEach((t) => { - if (d.data !== undefined) { - const newdocs = DocListCast(t.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - if (searchDocs.includes(t)) { - displaycheck = true; - } - }); - newdocs = newarray; - } - if (displaycheck === true) { - docsforFilter.push(d); - } + const docsforFilter: Doc[] = []; + childDocs.forEach((d) => { + if (DocUtils.Excluded(d, docFilters)) return; + let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + if (data !== undefined) { + let subDocs = DocListCast(data); + if (subDocs.length > 0) { + let newarray: Doc[] = []; + notFiltered = notFiltered || (!searchDocs.length && (!docFilters.length && !docRangeFilters.length) && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); + while (subDocs.length > 0 && !notFiltered) { + newarray = []; + subDocs.forEach((t) => { + const fieldKey = Doc.LayoutFieldKey(t); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); + DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + }); + subDocs = newarray; } } - if (searchDocs.includes(d)) { - docsforFilter.push(d); - } - }); - return docsforFilter; - } - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - return this.props.Document.dontRegisterView ? childDocs : DocUtils.FilterDocs(childDocs, this.docFilters(), docRangeFilters, viewSpecScript); + } + notFiltered && docsforFilter.push(d); + }); + return docsforFilter; } @action @@ -231,16 +224,22 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (docDragData) { let added = false; const dropAction = docDragData.dropAction || docDragData.userDropAction; - if ((!dropAction || dropAction === "move") && docDragData.moveDocument) { + const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); + const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); + if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop); + if ((!dropAction || dropAction === "move" || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); - const res = addedDocs.length ? this.addDocument(addedDocs) : true; if (movedDocs.length) { const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); - } else added = res; - e.stopPropagation(); + } else { + ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + added = addedDocs.length ? this.addDocument(addedDocs) : true; + } + added && e.stopPropagation(); + return added; } else { ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); @@ -249,7 +248,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: e.stopPropagation(); return added; } - else if (de.complete.annoDragData) { + else if (de.complete.annoDragData && (!this.props.isAnnotationOverlay || de.complete.annoDragData.dragDocument === this.props.Document)) { e.stopPropagation(); return this.addDocument(de.complete.annoDragData.dropDocument); } @@ -293,10 +292,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); + this.addDocument(Docs.Create.WebDocument(href, { ...options, _fitWidth: true, title: href })); } } else if (text) { - this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); + this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 })); } return; } @@ -342,7 +341,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc).text = text; this.props.addDocument(htmlDoc); if (srcWeb) { - const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0].contentDocument?.getSelection()?.focusNode as any); + const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0]?.contentDocument?.getSelection()?.focusNode as any); if (focusNode) { const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect(); const x = (rect?.x || 0); @@ -363,14 +362,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (uriList || text) { if ((uriList || text).includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) { const url = (uriList || text).replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; - this.addDocument(Docs.Create.VideoDocument(url, { + console.log("Video URI = ", uriList); + console.log("Add:" + this.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 - })); + }))); return; } // let matches: RegExpExecArray | null; @@ -391,27 +391,30 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // } } if (uriList) { + console.log("Web URI = ", uriList); const existingWebDoc = await Hypothesis.findWebDoc(uriList); if (existingWebDoc) { const alias = Doc.MakeAlias(existingWebDoc); alias.x = options.x; alias.y = options.y; alias._nativeWidth = 850; - alias._nativeHeight = 962; + alias._height = 512; alias._width = 400; this.addDocument(alias); } else { + console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList, { ...options, + _fitWidth: true, title: uriList.split("#annotations:")[0], _width: 400, - _height: 315, + _height: 512, _nativeWidth: 850, - _nativeHeight: 962, - UseCors: true + useCors: true }); + console.log(" ... " + newDoc.title); newDoc.data = new WebField(uriList.split("#annotations:")[0]); // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) - this.addDocument(newDoc); + console.log(" ... " + this.addDocument(newDoc) + " " + newDoc.title); } return; } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index c9bf82406..c5add7cfb 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -28,16 +28,7 @@ .no-indent { padding-left: 0; - } - - .bullet { - position: relative; - width: 15px; - color: $intermediate-color; - margin-top: 3px; - transform: scale(1.3, 1.3); - border: #80808030 1px solid; - border-radius: 4px; + width: max-content; } .editableView-container { @@ -85,73 +76,12 @@ white-space: pre-wrap; min-width: 10px; } - -.treeViewItem-openRight { - display: none; - height: 17px; - width: 15px; -} - -.treeViewItem-openRight:hover { - background: #797777; - cursor: pointer; -} - -.treeViewItem-border { - display: inherit; - border-left: dashed 1px #00000042; -} - -.treeViewItem-header-editing, -.treeViewItem-header { - border: transparent 1px solid; - display: flex; - - .editableView-container-editing-oneLine { - min-width: 15px; - } - - .documentView-node-topmost { - width: unset; - } - - >svg { - display: none; - } - -} - -.treeViewItem-header:hover { - .collectionTreeView-keyHeader { - display: inherit; - } - - >svg { - display: inherit; - } - - .treeViewItem-openRight { - display: inline-block; - height: 17px; - width: 15px; - - // display: inline; - svg { - display: block; - padding: 0px; - margin-left: 3px; - } - } -} - -.treeViewItem-header-above { - border-top: black 1px solid; -} - -.treeViewItem-header-below { - border-bottom: black 1px solid; -} - -.treeViewItem-header-inside { - border: black 1px solid; +.docContainer-system { + font-variant: all-small-caps; + border-radius: 5px; + background: grey; + color: white; + padding-left: 3px; + padding-right: 3px; + padding-bottom: 2px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index f23fa8eb6..a7c8a7cdc 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,667 +1,31 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { PrefetchProxy } from '../../../fields/Proxy'; -import { Document, listSpec } from '../../../fields/Schema'; -import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from '../../util/DocumentManager'; -import { SnappingManager } from '../../util/SnappingManager'; +import { Document } from '../../../fields/Schema'; +import { ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, Utils } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from "../../util/DragManager"; -import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; -import { MainView } from '../MainView'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; -import { DocumentView } from '../nodes/DocumentView'; -import { ImageBox } from '../nodes/ImageBox'; -import { KeyValueBox } from '../nodes/KeyValueBox'; -import { Templates } from '../Templates'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; -import { CollectionViewType } from './CollectionView'; +import { TreeView } from "./TreeView"; import React = require("react"); -import { makeTemplate } from '../../util/DropConverter'; -import { TraceMobx } from '../../../fields/util'; - -export interface TreeViewProps { - document: Doc; - dataDoc?: Doc; - libraryPath: Doc[] | undefined; - containingCollection: Doc; - prevSibling?: Doc; - renderDepth: number; - deleteDoc: (doc: Doc | Doc[]) => boolean; - moveDocument: DragManager.MoveFunction; - dropAction: dropActionType; - addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; - pinToPres: (document: Doc) => void; - panelWidth: () => number; - panelHeight: () => number; - ChromeHeight: undefined | (() => number); - addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; - indentDocument?: () => void; - outdentDocument?: () => void; - ScreenToLocalTransform: () => Transform; - backgroundColor?: (doc: Doc, renderDepth: number) => string | undefined; - outerXf: () => { translateX: number, translateY: number }; - treeViewDoc: Doc; - parentKey: string; - active: (outsideReaction?: boolean) => boolean; - treeViewHideHeaderFields: () => boolean; - treeViewPreventOpen: boolean; - renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle - onCheckedClick?: () => ScriptField; - onChildClick?: () => ScriptField; - ignoreFields?: string[]; -} - -@observer -/** - * Renders a treeView of a collection of documents - * - * special fields: - * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden - * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) - * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree - */ -class TreeView extends React.Component<TreeViewProps> { - private _editTitleScript: (() => ScriptField) | undefined; - private _header?: React.RefObject<HTMLDivElement> = React.createRef(); - private _treedropDisposer?: DragManager.DragDropDisposer; - private _dref = React.createRef<HTMLDivElement>(); - private _tref = React.createRef<HTMLDivElement>(); - private _docRef = React.createRef<DocumentView>(); - private _uniqueId = Utils.GenerateGuid(); - private _editMaxWidth: number | string = 0; - - get doc() { return this.props.document; } - get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } - get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs.length ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "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.treeViewPreventOpen) this._overrideTreeViewOpen = c; - else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; - } - @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } - @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } - @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } - @computed get dataDoc() { return this.doc[DataSym]; } - @computed get layoutDoc() { return Doc.Layout(this.doc); } - @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } - childDocList(field: string) { - const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; - return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCast(this.doc[field])); // otherwise use the document's data field - } - @computed get childDocs() { return this.childDocList(this.fieldKey); } - @computed get childLinks() { return this.childDocList("links"); } - @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } - @computed get boundsOfCollectionDocument() { - 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 openRight = () => this.props.addDocTab(this.doc, "onRight", this.props.libraryPath); - @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.doc !== target && this.props.deleteDoc(doc) && addDoc(doc); - } - @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); - } - @undoBatch @action removeDoc = (doc: Doc | Doc[]) => { - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true); - } - - constructor(props: any) { - super(props); - const script = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); - this._editTitleScript = script && (() => script); - if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false); - } - - protected createTreeDropTarget = (ele: HTMLDivElement) => { - this._treedropDisposer?.(); - ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); - } - - onPointerEnter = (e: React.PointerEvent): void => { - this.props.active(true) && Doc.BrushDoc(this.dataDoc); - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { - this._header!.current!.className = "treeViewItem-header"; - document.addEventListener("pointermove", this.onDragMove, true); - } - } - onPointerLeave = (e: React.PointerEvent): void => { - Doc.UnBrushDoc(this.dataDoc); - if (this._header?.current?.className !== "treeViewItem-header-editing") { - this._header!.current!.className = "treeViewItem-header"; - } - document.removeEventListener("pointermove", this.onDragMove, true); - } - onDragMove = (e: PointerEvent): void => { - Doc.UnBrushDoc(this.dataDoc); - 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 && this.childDocList.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"; - else if (!before) this._header!.current!.className += " treeViewItem-header-below"; - e.stopPropagation(); - } - - editableView = (key: string, style?: string) => (<EditableView - oneLine={true} - display={"inline-block"} - editing={true} - contents={StrCast(this.doc[key])} - height={12} - sizeToContent={true} - fontStyle={style} - fontSize={12} - GetValue={() => StrCast(this.doc[key])} - SetValue={undoBatch((value: string) => { - Doc.SetInPlace(this.doc, key, value, false) || true; - Doc.SetInPlace(this.doc, "editTitle", undefined, false); - })} - OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.doc, key, value, false); - const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(this.doc, "editTitle", undefined, false); - Doc.SetInPlace(doc, "editTitle", "*", false); - return this.props.addDocument(doc); - })} - onClick={() => { - SelectionManager.DeselectAll(); - Doc.UserDoc().activeSelection = new List([this.doc]); - return false; - }} - OnTab={undoBatch((shift?: boolean) => { - shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); - setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", "*", false), 0); - })} - />) - - preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { - const dragData = de.complete.docDragData; - dragData && (dragData.dropAction = this.props.treeViewDoc === dragData.treeViewDoc ? "same" : dragData.dropAction); - } - - @undoBatch - treeDrop = (e: Event, de: DragManager.DropEvent) => { - 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 && this.childDocList.length); - const complete = de.complete; - if (complete.linkDragData) { - const sourceDoc = complete.linkDragData.linkSourceDocument; - const destDoc = this.doc; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); - e.stopPropagation(); - } - const docDragData = complete.docDragData; - if (docDragData) { - e.stopPropagation(); - if (docDragData.draggedDocuments[0] === this.doc) return true; - const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - let addDoc = parentAddDoc; - if (inside) { - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( - (flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc), true) || parentAddDoc(doc); - } - const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; - return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); - } - return false; - } - - refTransform = (ref: HTMLDivElement) => { - const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); - const outerXf = this.props.outerXf(); - const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); - } - docTransform = () => this.refTransform(this._dref.current!); - getTransform = () => this.refTransform(this._tref.current!); - docWidth = () => { - const layoutDoc = this.layoutDoc; - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; - } - docHeight = () => { - const layoutDoc = this.layoutDoc; - const bounds = this.boundsOfCollectionDocument; - return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return this.docWidth() * aspect; - if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc._fitWidth ? (!this.doc._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; - })())); - } - - @computed get expandedField() { - const ids: { [key: string]: string } = {}; - const doc = this.doc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); - - const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; - const contents = doc[key]; - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - - if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce( - (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true); - contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : - DocListCast(contents), this.props.treeViewDoc, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, - this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields); - } else { - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={12} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; - } - rows.push(<div style={{ display: "flex" }} key={key}> - <span style={{ fontWeight: "bold" }}>{key + ":"}</span> - - {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; - } - - rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); - rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - - @computed get renderContent() { - TraceMobx(); - const expandKey = this.treeViewExpandedView; - if (["links", "annotations", this.fieldKey].includes(expandKey)) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => - (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); - const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; - const sortKey = `${this.fieldKey}-sortAscending`; - return <ul key={expandKey + "more"} onClick={(e) => { - this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true)); - e.stopPropagation(); - }}> - {!docs ? (null) : - TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc, - this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} - </ul >; - } else if (this.treeViewExpandedView === "fields") { - return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > - {this.expandedField} - </div></ul>; - } else { - const layoutDoc = this.layoutDoc; - const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; - const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> - <ContentFittingDocumentView - Document={layoutDoc} - DataDoc={this.dataDoc} - LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} - rootSelected={returnTrue} - treeViewDoc={undefined} - backgroundColor={this.props.backgroundColor} - fitToBox={this.boundsOfCollectionDocument !== undefined} - FreezeDimensions={true} - NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : returnZero} - NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : returnZero} - PanelWidth={panelWidth} - PanelHeight={panelHeight} - focus={returnFalse} - ScreenToLocalTransform={this.docTransform} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={this.props.containingCollection} - ContainingCollectionView={undefined} - addDocument={returnFalse} - moveDocument={this.props.moveDocument} - removeDocument={returnFalse} - parentActive={this.props.active} - whenActiveChanged={emptyFunction} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - ContentScaling={returnOne} - /> - </div>; - } - } - - get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } - - @action - bulletClick = (e: React.MouseEvent) => { - if (this.onCheckedClick && this.doc.type !== DocumentType.COL) { - // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; - this.onCheckedClick?.script.run({ - this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, - heading: this.props.containingCollection.title, - checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", - containingTreeView: this.props.treeViewDoc, - }, console.log); - } else { - this.treeViewOpen = !this.treeViewOpen; - } - e.stopPropagation(); - } - - @computed get renderBullet() { - TraceMobx(); - const checked = this.doc.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; - return <div className="bullet" - title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} - onClick={this.bulletClick} - style={{ color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> - {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs?.length ? "caret-square-right" : "caret-right") : (this.childDocs?.length ? "caret-square-down" : "caret-down"))} />} - </div>; - } - - showContextMenu = (e: React.MouseEvent) => { - this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - } - focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); - contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; - truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0); - showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); - onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.editTitleScript)); - /** - * Renders the EditableView title element for placement into the tree. - */ - @computed - get renderTitle() { - TraceMobx(); - const headerElements = this.props.treeViewHideHeaderFields() ? (null) : - <> - <FontAwesomeIcon icon="cog" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> - <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} - onPointerDown={action(() => { - if (this.treeViewOpen) { - this.doc.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : - this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : - this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : - (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : - this.childDocs.length ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); - } - this.treeViewOpen = true; - })}> - {this.treeViewExpandedView} - </span> - </>; - const view = this.showTitleEdit() ? this.editableView("title") : - <DocumentView - ref={this._docRef} - Document={this.doc} - DataDoc={undefined} - treeViewDoc={this.props.treeViewDoc} - LibraryPath={this.props.libraryPath || emptyPath} - addDocument={undefined} - addDocTab={this.props.addDocTab} - rootSelected={returnTrue} - pinToPres={emptyFunction} - onClick={this.onChildClick} - dropAction={this.props.dropAction} - moveDocument={this.move} - removeDocument={this.removeDoc} - ScreenToLocalTransform={this.getTransform} - ContentScaling={returnOne} - PanelWidth={this.truncateTitleWidth} - PanelHeight={returnZero} - NativeHeight={returnZero} - NativeWidth={returnZero} - contextMenuItems={this.contextMenuItems} - opacity={returnOne} - renderDepth={1} - focus={returnTrue} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - dontRegisterView={BoolCast(this.props.treeViewDoc.dontRegisterChildViews)} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={this.props.containingCollection} - />; - return <> - <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} - style={{ - fontWeight: this.doc.searchMatch ? "bold" : undefined, - textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, - outline: BoolCast(this.doc.workspaceBrush) ? "dashed 1px #06123232" : undefined, - pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none" - }} > - {view} - </div > - {headerElements} - <div className="treeViewItem-openRight" onClick={this.openRight}> - <FontAwesomeIcon title="open in a new pane" icon="external-link-alt" size="sm" /> - </div> - </>; - } - - render() { - TraceMobx(); - const sorting = this.doc[`${this.fieldKey}-sortAscending`]; - if (this.showTitleEdit()) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll - let par: any = this._header?.current; - if (par) { - while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; - if (par) { - const par_rect = (par as HTMLElement).getBoundingClientRect(); - const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect(); - this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0)); - } - } - } else this._editMaxWidth = ""; - return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> - <li className="collection-child"> - <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - SelectionManager.DeselectAll(); - } - }} - onPointerDown={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - } - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet} - {this.renderTitle} - </div> - <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} - </div> - </li> - </div>; - } - public static GetChildElements( - childDocs: Doc[], - treeViewDoc: Doc, - containingCollection: Doc, - dataDoc: Doc | undefined, - key: string, - parentCollectionDoc: Doc | undefined, - parentPrevSibling: Doc | undefined, - add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc | Doc[]) => boolean), - move: DragManager.MoveFunction, - dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => boolean, - pinToPres: (document: Doc) => void, - backgroundColor: undefined | ((document: Doc, renderDepth: number) => string | undefined), - screenToLocalXf: () => Transform, - outerXf: () => { translateX: number, translateY: number }, - active: (outsideReaction?: boolean) => boolean, - panelWidth: () => number, - ChromeHeight: undefined | (() => number), - renderDepth: number, - treeViewHideHeaderFields: () => boolean, - treeViewPreventOpen: boolean, - renderedIds: string[], - libraryPath: Doc[] | undefined, - onCheckedClick: undefined | (() => ScriptField), - onChildClick: undefined | (() => ScriptField), - ignoreFields: string[] | undefined - ) { - const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); - if (viewSpecScript) { - childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); - } - - const docs = childDocs.slice(); - const ascending = containingCollection?.[key + "-sortAscending"]; - if (ascending !== undefined) { - const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { - const reN = /[0-9]*$/; - const aA = a.replace(reN, ""); // get rid of trailing numbers - 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 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 - // return Number(descA.x) > Number(descB.x) ? 1 : -1; - // } - return first > second ? 1 : -1; - } - return ascending ? 1 : -1; - }); - } - - const rowWidth = () => panelWidth() - 20; - return docs.map((child, i) => { - const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); - if (!pair.layout || pair.data instanceof Promise) { - return (null); - } - - 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; - remove(child); - } - } - }; - 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 | Doc[], relativeTo?: Doc, before?: boolean) => { - return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); - }; - const childLayout = Doc.Layout(pair.layout); - 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]} - treeViewDoc={treeViewDoc} - key={child[Id]} - indentDocument={indent} - outdentDocument={outdent} - onCheckedClick={onCheckedClick} - onChildClick={onChildClick} - renderDepth={renderDepth} - deleteDoc={remove} - addDocument={addDocument} - backgroundColor={backgroundColor} - panelWidth={rowWidth} - panelHeight={rowHeight} - ChromeHeight={ChromeHeight} - moveDocument={move} - dropAction={dropAction} - addDocTab={addDocTab} - pinToPres={pinToPres} - ScreenToLocalTransform={screenToLocalXf} - outerXf={outerXf} - parentKey={key} - active={active} - treeViewHideHeaderFields={treeViewHideHeaderFields} - treeViewPreventOpen={treeViewPreventOpen} - renderedIds={renderedIds} - ignoreFields={ignoreFields} />; - }); - } -} +import { DocumentManager } from '../../util/DocumentManager'; +import { FormattedTextBoxComment } from '../nodes/formattedText/FormattedTextBoxComment'; export type collectionTreeViewProps = { treeViewHideTitle?: boolean; @@ -673,16 +37,16 @@ export type collectionTreeViewProps = { @observer export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { private treedropDisposer?: DragManager.DragDropDisposer; + private _isChildActive = false; private _mainEle?: HTMLDivElement; + public _uniqueId = Utils.GenerateGuid(); - @computed get doc() { return this.props.Document; } + @computed get doc() { TraceMobx(); return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer?.(); - if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); - } + if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); } protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { @@ -699,18 +63,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.treedropDisposer?.(); } - @action - remove = (doc: Doc | Doc[]): boolean => { + @undoBatch + remove = action((doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; const targetDataDoc = this.doc[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); + SelectionManager.DeselectAll(); if (result.length !== value.length) { + const ind = targetDataDoc[this.props.fieldKey].indexOf(doc); targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + if (ind > 0) { + const prev = targetDataDoc[this.props.fieldKey][ind - 1]; + FormattedTextBox.SelectOnLoad = prev[Id]; + const prevView = DocumentManager.Instance.getDocumentView(prev, this.props.CollectionView); + prevView?.select(false); + } return true; } return false; - } + }); @action addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => @@ -718,172 +90,150 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); if (this.doc.resolvedDataDoc instanceof Promise) { this.doc.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); + } else if (relativeTo === undefined) { + this.props.addDocument(doc); } else { doAddDoc(doc); + (doc instanceof Doc ? [doc] : doc).forEach(d => d.context = this.props.Document); } return true; } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myWorkspaces) { - ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.doc), icon: "minus" }); - e.stopPropagation(); - e.preventDefault(); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosed) { - ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" }); - e.stopPropagation(); - e.preventDefault(); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else { + if (!Doc.UserDoc().noviceMode) { const layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: (this.doc.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.doc.treeViewPreventOpen = !this.doc.treeViewPreventOpen, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideLinkLines ? "Show" : "Hide") + " Link Lines", event: () => this.doc.treeViewHideLinkLines = !this.doc.treeViewHideLinkLines, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } - !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ - description: "Buxton Layout", icon: "eye", event: () => { - const { ImageDocument, PdfDocument } = Docs.Create; - const { Document } = this.props; - const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; - const detailView = Cast(Cast(Doc.UserDoc()["template-button-detail"], Doc, null)?.dragFactory, Doc, null); - 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"; - - const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target - let pdfContent: string; - this.childDocs?.map(d => { - DocListCast(d.data).map((img, i) => { - const caption = (d.captions as any)[i]; - if (caption) { - Doc.GetProto(img).caption = caption; - Doc.GetProto(img).doubleClickView = (pdfContent = StrCast(img.additionalMedia_pdfs)) ? PdfDocument(pdfContent, { title: pdfContent }) : fallback; - } - }); - Doc.GetProto(d).type = "buxton"; - Doc.GetProto(d).proto = heroView; // all devices "are" heroViews that share the same layout & defaults. Seems better than making them all be independent and copy a layout string // .layout = ImageBox.LayoutString("hero"); - }); - - const iconBuxtonView = ImageDocument(fallbackImg, { title: "hero", _width: 60, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); - iconBuxtonView.isTemplateDoc = makeTemplate(iconBuxtonView, true, "icon_buxton"); - Doc.UserDoc()["template-icon-view-buxton"] = new PrefetchProxy(iconBuxtonView); - const tempIcons = Doc.GetProto(Cast(Doc.UserDoc()["template-icons"], Doc, null)); - Doc.AddDocToList(tempIcons, "data", iconBuxtonView); - - Document.childLayoutTemplate = heroView; - Document.childClickedOpenTemplateView = new PrefetchProxy(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", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" - }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); @computed get renderClearButton() { - return <div id="toolbar" key="toolbar"> - <button className="toolbar-button round-button" title="Empty" - onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> + return !this.doc.allowClear ? (null) : <div key="toolbar"> + <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> <FontAwesomeIcon icon={"trash"} size="sm" /> </button> </div >; } - onChildClick = () => { - return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); + @undoBatch + makeTextCollection = action((childDocs: Doc[]) => { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + const bullet = TreeView.makeTextBullet(); + bullet.context = this.doc; + this.addDoc(bullet, childDocs.length ? childDocs[0] : undefined, true); + }); + + editableTitle = (childDocs: Doc[]) => { + return !this.dataDoc ? (null) : <EditableView + contents={this.dataDoc.title} + editing={false} + display={"block"} + maxHeight={72} + height={"auto"} + GetValue={() => StrCast(this.dataDoc.title)} + SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { + if (this.props.Document.treeViewOutlineMode && enter) { + this.makeTextCollection(childDocs); + } + return Doc.SetInPlace(this.dataDoc, "title", value, false); + })} />; } - render() { + + + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.PanelWidth() - 20); + rtfOutlineHeight = () => Math.min(this.layoutDoc?.[HeightSym](), (StrCast(this.layoutDoc?._fontSize) ? Number(StrCast(this.layoutDoc?._fontSize, "32px").replace("px", "")) : NumCast(this.layoutDoc?._fontSize)) * 2); + titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); + documentTitle = (childDocs: Doc[]) => { + return <div style={{ display: "inline-block", height: this.rtfOutlineHeight() }} key={this.doc[Id]} + onKeyDown={e => { + e.stopPropagation(); + e.key === "Enter" && this.makeTextCollection(childDocs); + }}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LayoutTemplateString={FormattedTextBox.LayoutString("text")} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + //dontRegisterView={true} + backgroundColor={this.props.backgroundColor} + PanelWidth={this.rtfWidth} + PanelHeight={this.rtfOutlineHeight} + focus={this.props.focus} + ScreenToLocalTransform={this.titleTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.doc} + ContainingCollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={returnFalse} + removeDocument={returnFalse} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div>; + } + + onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); + whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; + active = (outsideReaction: boolean | undefined) => this.props.active(outsideReaction) || this._isChildActive; + @computed get treeChildren() { + TraceMobx(); + return this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; + } + @computed get treeViewElements() { TraceMobx(); - if (!(this.doc instanceof Doc)) return (null); const dropAction = StrCast(this.doc.childDropAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc); - const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; - return !childDocs ? (null) : ( + return TreeView.GetChildElements(this.treeChildren, this, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.outerXf, this.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), + BoolCast(this.doc.treeViewPreventOpen), [], this.props.onCheckedClick, + this.onChildClick, this.props.ignoreFields, true, this.whenActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.dontRegisterChildViews, "boolean", null)); + } + @computed get titleBar() { + const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; + return hideTitle ? (null) : (this.doc.treeViewOutlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); + } + render() { + TraceMobx(); + if (!(this.doc instanceof Doc)) return (null); + const background = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.doc.backgroundColor) || this.props.backgroundColor?.(this.doc, this.props.renderDepth); + const paddingX = `${NumCast(this.doc._xPadding, 10)}px`; + const paddingTop = `${NumCast(this.doc._yPadding, 20)}px`; + const pointerEvents = !this.props.active() && !SnappingManager.GetIsDragging() && !this._isChildActive ? "none" : undefined; + + return !this.treeChildren ? (null) : ( <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> - <div className="collectionTreeView-dropTarget" id="body" - style={{ - background: this.props.backgroundColor?.(this.doc, this.props.renderDepth), - paddingLeft: `${NumCast(this.doc._xPadding, 10)}px`, - paddingRight: `${NumCast(this.doc._xPadding, 10)}px`, - paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined - }} + <div className="collectionTreeView-dropTarget" + style={{ background, paddingLeft: paddingX, paddingRight: paddingX, paddingTop, pointerEvents }} onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - {this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : <EditableView - contents={this.dataDoc.title} - editing={false} - display={"block"} - maxHeight={72} - height={"auto"} - GetValue={() => StrCast(this.dataDoc.title)} - SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} - OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.dataDoc, "title", value, false); - const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(doc, "editTitle", "*", false); - this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); - })} />} - {this.doc.allowClear ? this.renderClearButton : (null)} - <ul className="no-indent" style={{ width: "max-content" }} > - { - TreeView.GetChildElements(childDocs, this.doc, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), - BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.onChildClick, this.props.ignoreFields) - } + {this.titleBar} + {this.renderClearButton} + <ul className="no-indent"> + {this.treeViewElements} </ul> </div > </div> ); } -} - -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 => { - const doc = new Doc(); - doc.system = true; - doc.title = facetValue.toString(); - doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); - return doc; - }); - return new List<Doc>(facetValueDocSet); -}); - -Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { - const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); - for (let i = 0; i < docFilters.length; i += 3) { - const [header, value, state] = docFilters.slice(i, i + 3); - if (header === facetHeader && value === facetValue) { - return state; - } - } - return undefined; -});
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2064b0be4..522f46280 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,24 +1,17 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faCopy, faEllipsisV, faFingerprint, faGlobeAmericas, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable } 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 '../../../fields/DateField'; -import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate, AclAdmin } from '../../../fields/Doc'; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { RichTextField } from '../../../fields/RichTextField'; -import { listSpec } from '../../../fields/Schema'; -import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; +import { returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -26,6 +19,7 @@ import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { UndoManager } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from '../ContextMenuItem'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { Touchable } from '../Touchable'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; @@ -40,21 +34,16 @@ import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultir import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; -import { CollectionStaffView } from './CollectionStaffView'; import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { table } from 'console'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy); - export enum CollectionViewType { Invalid = "invalid", Freeform = "freeform", @@ -76,10 +65,9 @@ export enum CollectionViewType { } export interface CollectionViewCustomProps { filterAddDocument?: (doc: Doc | Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) - childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection - childLayoutString?: string; // specify a layout string to use for children of the collection childOpacity?: () => number; hideFilter?: true; + childIgnoreNativeSize?: boolean; } export interface CollectionRenderProps { @@ -90,8 +78,8 @@ export interface CollectionRenderProps { whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; PanelHeight: () => number; - ChildLayoutTemplate?: () => Doc; - ChildLayoutString?: string; + ChildLayoutTemplate?: () => Doc;// specify a layout Doc template to use for children of the collection + ChildLayoutString?: string;// specify a layout string to use for children of the collection } @observer @@ -99,8 +87,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } _isChildActive = false; //TODO should this be observable? - get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } - set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } + get _isLightboxOpen() { return BoolCast(this.props.Document._isLightboxOpen); } + set _isLightboxOpen(value) { this.props.Document._isLightboxOpen = value; } @observable private _curLightboxImg = 0; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } @@ -156,13 +144,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); + //else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); } }); } if (effectiveAcl === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); + added.map(doc => { + Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + doc.context = this.props.Document; + }); } else { added.map(doc => { @@ -183,12 +175,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus doc._stayInCollection = undefined; doc.context = this.props.Document; }); - added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); } } } @@ -198,16 +186,21 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { + const docAcl = GetEffectiveAcl(doc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { - Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); - recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + const ind = (targetDataDoc[this.props.fieldKey] as List<Doc>).indexOf(doc); + (targetDataDoc[this.props.fieldKey] as List<Doc>).splice(ind, 0); + if (ind !== -1) { + Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); + recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } }); return true; } @@ -250,7 +243,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); - private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { + private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { + TraceMobx(); const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); @@ -269,15 +263,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus case CollectionViewType.Map: return (<CollectionMapView key="collview" {...props} />); case CollectionViewType.Grid: return (<CollectionGridView key="gridview" {...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} ChildLayoutString={props.ChildLayoutString} />); } } } - private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - return this.SubViewHelper(type, renderProps); - } - - setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { const subItems: ContextMenuProps[] = []; @@ -287,23 +276,24 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } subItems.push({ description: "Schema", event: () => func(CollectionViewType.Schema), icon: "th-list" }); subItems.push({ description: "Tree", event: () => func(CollectionViewType.Tree), icon: "tree" }); - subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); - subItems.push({ description: "Stacking (AutoHeight)", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => func(CollectionViewType.Staff), icon: "music" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); + subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); subItems.push({ description: "Multicolumn", event: () => func(CollectionViewType.Multicolumn), icon: "columns" }); subItems.push({ description: "Multirow", event: () => func(CollectionViewType.Multirow), icon: "columns" }); subItems.push({ description: "Masonry", event: () => func(CollectionViewType.Masonry), icon: "columns" }); subItems.push({ description: "Carousel", event: () => func(CollectionViewType.Carousel), icon: "columns" }); subItems.push({ description: "3D Carousel", event: () => func(CollectionViewType.Carousel3D), icon: "columns" }); - subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); - subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); - addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); + subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); - const existingVm = ContextMenu.Instance.findByDescription(category); - const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); - !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: "eye" }); + if (!Doc.IsSystem(this.props.Document) && !this.props.Document.annotationOn) { + const existingVm = ContextMenu.Instance.findByDescription(category); + const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); + !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: "eye" }); + } } onContextMenu = (e: React.MouseEvent): void => { @@ -312,7 +302,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.props.Document); newRendition._viewType = vtype; - this.props.addDocTab(newRendition, "onRight"); + this.props.addDocTab(newRendition, "add:right"); return newRendition; }, false); @@ -320,34 +310,34 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const optionItems = options && "subitems" in options ? options.subitems : []; !Doc.UserDoc().noviceMode ? optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }) : null; if (this.props.Document.childLayout instanceof Doc) { - optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "add:right"), icon: "project-diagram" }); } if (this.props.Document.childClickedOpenTemplateView instanceof Doc) { - optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "add:right"), icon: "project-diagram" }); } !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - const funcs = [ - { key: "onChildClick", name: "On Child Clicked" }, - { key: "onChildDoubleClick", name: "On Child Double Clicked" }]; - funcs.map(func => onClicks.push({ - description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { - const alias = Doc.MakeAlias(this.props.Document); - DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, "onRight"); - } - })); - DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => - onClicks.push({ - description: `Set child ${childClick.title}`, - icon: "edit", - event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + if (!Doc.UserDoc().noviceMode && !this.props.Document.annotationOn) { + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + const funcs = [{ key: "onChildClick", name: "On Child Clicked" }, { key: "onChildDoubleClick", name: "On Child Double Clicked" }]; + funcs.map(func => onClicks.push({ + description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { + const alias = Doc.MakeAlias(this.props.Document); + DocUtils.makeCustomViewClicked(alias, undefined, func.key); + this.props.addDocTab(alias, "add:right"); + } })); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => + onClicks.push({ + description: `Set child ${childClick.title}`, + icon: "edit", + event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + })); + !Doc.IsSystem(this.props.Document) && !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + } if (!Doc.UserDoc().noviceMode) { const more = cm.findByDescription("More..."); @@ -358,222 +348,31 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } } - lightbox = (images: string[]) => { + lightbox = (images: { image: string, title: string, caption: 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); + const mainPath = path.extname(images[this._curLightboxImg].image); + const nextPath = path.extname(images[(this._curLightboxImg + 1) % images.length].image); + const prevPath = path.extname(images[(this._curLightboxImg + images.length - 1) % images.length].image); + const main = images[this._curLightboxImg].image.replace(mainPath, "_o" + mainPath); + const title = images[this._curLightboxImg].title; + const caption = images[this._curLightboxImg].caption; + const next = images[(this._curLightboxImg + 1) % images.length].image.replace(nextPath, "_o" + nextPath); + const prev = images[(this._curLightboxImg + images.length - 1) % images.length].image.replace(prevPath, "_o" + prevPath); return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox" mainSrc={main} nextSrc={next} prevSrc={prev} + imageTitle={title} + imageCaption={caption} 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)} />); } - get _facetWidth() { return NumCast(this.props.Document._facetWidth); } - set _facetWidth(value) { this.props.Document._facetWidth = value; } bodyPanelWidth = () => this.props.PanelWidth(); - facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); - - @computed get dataDoc() { - return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : - this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template - } - // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. - // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through - // to its children which may be templates. - // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' - @computed get dataField() { - return this.dataDoc[this.props.fieldKey]; - } - - get childLayoutPairs(): { layout: Doc; data: Doc; }[] { - const { Document, DataDoc } = this.props; - const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); - return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types - } - get childDocList() { - return Cast(this.dataField, listSpec(Doc)); - } - get childDocs() { - const dfield = this.dataField; - const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []); - const docs = rawdocs.filter(d => d && !(d instanceof Promise)).map(d => d as Doc); - const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); - return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - } - @computed get _allFacets() { - TraceMobx(); - const facets = new Set<string>(["type", "text", "data", "author", "ACL"]); - this.childDocs.filter(child => child).forEach(child => child && Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.filter(child => child).forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets).filter(f => !f.startsWith("_") && !["proto", "zIndex", "isPrototype", "context", "text-noTemplate"].includes(f)).sort(); - } - - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const facetCollection = this.props.Document; - const found = DocListCast(facetCollection[this.props.fieldKey + "-filter"]).findIndex(doc => doc.title === facetHeader); - if (found !== -1) { - (facetCollection[this.props.fieldKey + "-filter"] as List<Doc>).splice(found, 1); - const docFilter = Cast(this.props.Document._docFilters, listSpec("string")); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); - } - } - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string")); - if (docRangeFilters) { - let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); - } - } - } else { - const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - var rtfields = 0; - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => { - const field = child[facetHeader] as Field; - const fieldStr = Field.toString(field); - if (field instanceof RichTextField || (typeof (field) === "string" && fieldStr.split(" ").length > 2)) rtfields++; - return set.add(fieldStr); - }, new Set<string>())); - - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - let newFacet: Opt<Doc>; - if (facetHeader === "text" || rtfields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet.target = this.props.Document; - newFacet._textBoxPadding = 4; - const scriptText = `setDocFilter(this.target, "${facetHeader}", text, "match")`; - newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); - } else if (nonNumbers / facetValues.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader, treeViewExpandedView: "layout", treeViewOpen: true }); - const newFacetField = Doc.LayoutFieldKey(newFacet); - const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); - const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); - newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; - newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; - Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; - newFacet.target = this.props.Document; - const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); - Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } else { - newFacet = new Doc(); - newFacet.sytem = true; - newFacet.title = facetHeader; - newFacet.treeViewOpen = true; - newFacet.type = DocumentType.COL; - const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; - newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, {}, capturedVariables); - } - newFacet && Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } - } - - onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._facetWidth = this.props.PanelWidth() - Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); - return false; - }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); - } - - filterBackground = () => "rgba(105, 105, 105, 0.432)"; - get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) - @computed get scriptField() { - const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); - return script ? () => script : undefined; - } - @computed get filterView() { - TraceMobx(); - const facetCollection = this.props.Document; - const flyout = ( - <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div> - ); - - return !this._facetWidth || this.props.dontRegisterView ? (null) : <div className="collectionTimeView-treeView" style={{ width: `${this.facetWidth()}px`, overflow: this.facetWidth() < 15 ? "hidden" : undefined }}> - <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="collectionTimeView-button"> - <FontAwesomeIcon icon={faEdit} size={"lg"} /> - <span className="collectionTimeView-span">Facet Filters</span> - </div> - </Flyout> - </div> - <div className="collectionTimeView-tree" key="tree"> - <CollectionTreeView - PanelPosition={""} - Document={facetCollection} - DataDoc={facetCollection} - fieldKey={`${this.props.fieldKey}-filter`} - CollectionView={this} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - PanelWidth={this.facetWidth} - PanelHeight={this.props.PanelHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} - LibraryPath={emptyPath} - rootSelected={this.props.rootSelected} - renderDepth={1} - dropAction={this.props.dropAction} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - addDocTab={returnFalse} - pinToPres={returnFalse} - isSelected={returnFalse} - select={returnFalse} - bringToFront={emptyFunction} - active={this.props.active} - whenActiveChanged={returnFalse} - treeViewHideTitle={true} - ContentScaling={returnOne} - focus={returnFalse} - treeViewHideHeaderFields={true} - onCheckedClick={this.scriptField} - ignoreFields={this.ignoreFields} - annotationsKey={""} - dontRegisterView={true} - backgroundColor={this.filterBackground} - moveDocument={returnFalse} - removeDocument={returnFalse} - addDocument={returnFalse} /> - </div> - </div>; - } childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); - childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); + @computed get childLayoutString() { return StrCast(this.props.Document.childLayoutString); } render() { TraceMobx(); @@ -588,25 +387,19 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; - const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : - `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; + const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.treeViewOutlineMode || this.props.Document._isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : + `${CurrentUserUtils.ActiveDashboard?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; return (<div className={"collectionView"} onContextMenu={this.onContextMenu} - style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> + style={{ pointerEvents: this.props.Document._isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} - <div className="collectionView-facetCont" style={{ display: this.props.PanelPosition === "absolute" ? "flex" : "", justifyContent: this.props.PanelPosition === "absolute" ? "center" : "", width: `calc(100% - ${this.facetWidth()}px)` }}> - {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} - </div> - {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => - Cast(d.data, ImageField) ? - (Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ? - Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href - : - ""))} - {(Doc.UserDoc()?.noviceMode || !this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : - <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} - style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "60%" }} /> - } - {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView} + {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => Cast(d.data, ImageField, null)).map(d => + ({ + image: (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, + title: StrCast(d.title), + caption: Field.toString(d.caption as Field) + })))} </div>); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx deleted file mode 100644 index 149d4927b..000000000 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from "react"; -import './ParentDocumentSelector.scss'; -import { Doc } from "../../../fields/Doc"; -import { observer } from "mobx-react"; -import { observable, action, runInAction, trace, computed, reaction, IReactionDisposer } from "mobx"; -import { Id } from "../../../fields/FieldSymbols"; -import { SearchUtil } from "../../util/SearchUtil"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast, StrCast } from "../../../fields/Types"; -import { CollectionViewType } from "./CollectionView"; -import { DocumentButtonBar } from "../DocumentButtonBar"; -import { DocumentManager } from "../../util/DocumentManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { DocumentView } from "../nodes/DocumentView"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Tooltip } from "@material-ui/core"; -const higflyout = require("@hig/flyout"); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - -library.add(faCog); - -type SelectorProps = { - Document: Doc, - Stack?: any, - addDocTab(doc: Doc, location: string): void -}; - -@observer -export class SelectorContextMenu extends React.Component<SelectorProps> { - @observable private _docs: { col: Doc, target: Doc }[] = []; - @observable private _otherDocs: { col: Doc, target: Doc }[] = []; - _reaction: IReactionDisposer | undefined; - - componentDidMount() { - this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); - } - componentWillUnmount() { - this._reaction?.(); - } - async fetchDocuments() { - const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)); - const containerProtoSets = await Promise.all(aliases.map(async alias => - ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); - const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => { - return (SearchUtil.GetAliasesOfDocument(container)); - })); - const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => { - return (SearchUtil.GetAliasesOfDocument(dp)); - })); - const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); - runInAction(() => { - this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); - this._otherDocs = []; - }); - } - - getOnClick({ col, target }: { col: Doc, target: Doc }) { - return () => { - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (col._viewType === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target._width) / 2; - const newPanY = NumCast(target.y) + NumCast(target._height) / 2; - col._panX = newPanX; - col._panY = newPanY; - } - this.props.addDocTab(col, "inTab"); // bcz: dataDoc? - }; - } - - render() { - return <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?.toString()}</a></p>)} - {this._otherDocs.length ? <hr key="hr" /> : null} - {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> { - render() { - const flyout = ( - <div className="parentDocumentSelector-flyout" title=" "> - <SelectorContextMenu {...this.props} /> - </div> - ); - return <div title="Show Contexts" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <span className="parentDocumentSelector-button" > - <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> - </span> - </Flyout> - </div>; - } -} - -@observer -export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> { - customStylesheet(styles: any) { - return { - ...styles, - panel: { - ...styles.panel, - minWidth: "100px" - }, - }; - } - _ref = React.createRef<HTMLDivElement>(); - - @computed get flyout() { - return ( - <div className="ParentDocumentSelector-flyout" title=" " ref={this._ref}> - <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> - </div> - ); - } - - render() { - return <Tooltip title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> - <span onPointerDown={e => { - if (getComputedStyle(this._ref.current!).width !== "100%") { - e.stopPropagation(); e.preventDefault(); - } - this.props.views()[0]?.select(false); - }} className="buttonSelector"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> - <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> - </Flyout> - </span> - </Tooltip>; - } -} diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index abbb7f6b5..eda62bf0b 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -5,16 +5,15 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; -import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt, AclPrivate, AclReadonly, DataSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, returnEmptyDoclist } from "../../../Utils"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -22,11 +21,15 @@ import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; +import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell } from "./CollectionSchemaCells"; import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { GetEffectiveAcl } from "../../../fields/util"; +import { DateField } from "../../../fields/DateField"; +import { ImageField } from "../../../fields/URLField"; enum ColumnType { @@ -45,7 +48,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); export interface SchemaTableProps { @@ -70,7 +73,7 @@ export interface SchemaTableProps { isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; setFocused: (document: Doc) => void; - setPreviewDoc: (document: Doc) => void; + setPreviewDoc: (document: Opt<Doc>) => void; columns: SchemaHeaderField[]; documentKeys: any[]; headerIsEditing: boolean; @@ -91,7 +94,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @observable _cellIsEditing: boolean = false; @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _openCollections: Array<string> = []; + @observable _openCollections: Set<number> = new Set; @observable _showDoc: Doc | undefined; @observable _showDataDoc: any = ""; @@ -136,16 +139,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeSorting = (col: any) => { - if (col.desc === undefined) { - // no sorting - this.props.changeColumnSort(col, true); - } else if (col.desc === true) { - // descending sort - this.props.changeColumnSort(col, false); - } else if (col.desc === false) { - // ascending sort - this.props.changeColumnSort(col, undefined); - } + this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true); } @action @@ -153,7 +147,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { - const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); const columns: Column<Doc>[] = []; const tableIsFocused = this.props.isFocused(this.props.Document, false); @@ -161,27 +154,16 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const focusedCol = this._focusedCell.col; const isEditable = !this.props.headerIsEditing; - if (this.childDocs.reduce((found, doc) => found || doc.type === DocumentType.COL, false)) { - columns.push( - { - expander: true, - Header: "", - width: 30, - Expander: (rowInfo) => { - if (rowInfo.original.type === "collection") { - return rowInfo.isExpanded ? - <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"caret-down"} size="sm" /></div> : - <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"caret-right"} size="sm" /></div>; - } else { - return null; - } - } - } - ); - } - this.props.active; - - const cols = this.props.columns.map(col => { + columns.push({ + expander: true, Header: "", width: 58, + Expander: (rowInfo) => { + return rowInfo.original.type !== DocumentType.COL ? (null) : + <div className="collectionSchemaView-expander" onClick={action(() => (this._openCollections[rowInfo.isExpanded ? "delete" : "add"])(rowInfo.viewIndex))}> + <FontAwesomeIcon icon={rowInfo.isExpanded ? "caret-down" : "caret-right"} size="lg" /> + </div>; + } + }); + columns.push(...this.props.columns.map(col => { const icon: IconProp = this.getColumnType(col) === ColumnType.Number ? "hashtag" : this.getColumnType(col) === ColumnType.String ? "font" : this.getColumnType(col) === ColumnType.Boolean ? "check-square" : this.getColumnType(col) === ColumnType.Doc ? "file" : this.getColumnType(col) === ColumnType.Image ? "image" : this.getColumnType(col) === ColumnType.List ? "list-ul" : @@ -209,25 +191,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { width={"100%"} />; - - const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; - - const header = - <div //className="collectionSchemaView-header" - //onClick={e => this.props.openHeader(col, menuContent, e.clientX, e.clientY)} - className="collectionSchemaView-menuOptions-wrapper" - style={{ - background: col.color, padding: "2px", - display: "flex", cursor: "default", height: "100%", - }}> - {/* <FontAwesomeIcon onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> */} - {keysDropdown} - <div onClick={e => this.changeSorting(col)} - style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> - <FontAwesomeIcon icon={sortIcon} size="lg" /> - </div> - </div>; + const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> + {keysDropdown} + <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + <FontAwesomeIcon icon={sortIcon} size="lg" /> + </div> + </div>; return { Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, @@ -260,21 +230,22 @@ export class SchemaTable extends React.Component<SchemaTableProps> { showDoc: this.showDoc, }; - 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} />; - if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />; - if (colType === ColumnType.Image) return <CollectionSchemaImageCell {...props} />; - if (colType === ColumnType.List) return <CollectionSchemaListCell {...props} />; - if (colType === ColumnType.Date) return <CollectionSchemaDateCell {...props} />; - return <CollectionSchemaCell {...props} />; + + switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) { + case ColumnType.Number: return <CollectionSchemaNumberCell {...props} />; + case ColumnType.String: return <CollectionSchemaStringCell {...props} />; + case ColumnType.Boolean: return <CollectionSchemaCheckboxCell {...props} />; + case ColumnType.Doc: return <CollectionSchemaDocCell {...props} />; + case ColumnType.Image: return <CollectionSchemaImageCell {...props} />; + case ColumnType.List: return <CollectionSchemaListCell {...props} />; + case ColumnType.Date: return <CollectionSchemaDateCell {...props} />; + default: + return <CollectionSchemaCell {...props} />; + } }, minWidth: 200, }; - }); - columns.push(...cols); - + })); columns.push({ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, accessor: (doc: Doc) => 0, @@ -283,8 +254,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const rowIndex = rowProps.index; const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - const props: CellProps = { - row: rowIndex, + return <CollectionSchemaButtons {...{ + row: rowProps.index, col: columnIndex, rowProps: rowProps, isFocused: isFocused, @@ -303,9 +274,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed: this.setComputed, getField: this.getField, showDoc: this.showDoc, - }; - - return <CollectionSchemaButtons {...props} />; + }} />; }, width: 28, resizable: false @@ -316,12 +285,7 @@ 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) - const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); - if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { - const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); - } else if (this.props.Document._schemaHeaders === undefined) { + if (this.props.Document._schemaHeaders === undefined) { this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); } @@ -336,7 +300,15 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + const tableDoc = this.props.Document[DataSym]; + const effectiveAcl = GetEffectiveAcl(tableDoc); + + if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) { + doc.context = this.props.Document; + tableDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + return false; } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -362,19 +334,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true); // TODO: editing border doesn't work :( return { - style: { - border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" - } + style: { border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } }; } - @action - onCloseCollection = (collection: Doc): void => { - const index = this._openCollections.findIndex(col => col === collection[Id]); - if (index > -1) this._openCollections.splice(index, 1); - } - - @action onExpandCollection = (collection: Doc) => this._openCollections.push(collection[Id]); @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing; @action @@ -385,7 +348,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); pdoc && this.props.setPreviewDoc(pdoc); - } else if ((this._cellIsEditing || this.props.headerIsEditing) && (e.keyCode === 37 || e.keyCode === 39)) { + e.stopPropagation(); + } else if (e.keyCode === 27) { + this.props.setPreviewDoc(undefined); e.stopPropagation(); // stopPropagation for left/right arrows } } @@ -410,9 +375,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @undoBatch - createRow = () => { + createRow = action(() => { this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - } + this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; + }); @undoBatch @action @@ -427,22 +393,21 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - getColumnType = (column: SchemaHeaderField): ColumnType => { - // added functionality to convert old column type stuff to new column type stuff -syip + getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => { + if (doc && field && column.type === ColumnType.Any) { + const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)]; + if (val instanceof ImageField) return ColumnType.Image; + if (val instanceof Doc) return ColumnType.Doc; + if (val instanceof DateField) return ColumnType.Date; + if (val instanceof List) return ColumnType.List; + } if (column.type && column.type !== 0) { return column.type; } if (columnTypes.get(column.heading)) { - column.type = columnTypes.get(column.heading)!; - return columnTypes.get(column.heading)!; - } - const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - if (!typesDoc) { - column.type = ColumnType.Any; - return ColumnType.Any; + return column.type = columnTypes.get(column.heading)!; } - column.type = NumCast(typesDoc[column.heading]); - return NumCast(typesDoc[column.heading]); + return column.type = ColumnType.Any; } @undoBatch @@ -471,11 +436,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get reactTable() { 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); + const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false); + const expanded: { [name: string]: any } = {}; + Array.from(this._openCollections.keys()).map(col => expanded[col.toString()] = true); const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable @@ -493,17 +456,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { expanded={expanded} resized={this.resized} onResizedChange={this.props.onResizedChange} - SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) : + SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== DocumentType.COL) ? (null) : <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} /></div>} />; } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); - } + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); } getField = (row: number, col?: number) => { @@ -564,10 +524,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { script = `const $ = (row:number, col?:number) => { - if(col === undefined) { - return (doc as any)[key][row + ${row}]; - } - return (doc as any)[key][row + ${row}][(doc as any)._schemaHeaders[col + ${col}].heading]; + const rval = (doc as any)[key][row + ${row}]; + return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; } return ${script}`; const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); @@ -587,9 +545,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } onOpenClick = () => { - if (this._showDoc) { - this.props.addDocTab(this._showDoc, "onRight"); - } + this._showDoc && this.props.addDocTab(this._showDoc, "add:right"); } getPreviewTransform = (): Transform => { @@ -598,14 +554,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { render() { const preview = ""; - return <div className="collectionSchemaView-table" style={{ overflow: this.props.Document._searchDoc ? undefined : "auto" }} + return <div className="collectionSchemaView-table" onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {StrCast(this.props.Document.type) !== "search" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + {StrCast(this.props.Document._chromeStatus) !== "disabled" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> : undefined} {!this._showDoc ? (null) : - <div className="collectionSchemaView-documentPreview" //onClick={() => { this.onOpenClick(); }} + <div className="collectionSchemaView-documentPreview" style={{ position: "absolute", width: 150, height: 150, background: "dimGray", display: "block", top: 0, left: 0, @@ -614,8 +570,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { ref="overlay"><ContentFittingDocumentView Document={this._showDoc} DataDoc={this._showDataDoc} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={true} FreezeDimensions={true} focus={emptyFunction} @@ -626,6 +580,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { PanelHeight={() => 150} ScreenToLocalTransform={this.getPreviewTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss new file mode 100644 index 000000000..edf556c9f --- /dev/null +++ b/src/client/views/collections/TabDocView.scss @@ -0,0 +1,54 @@ +input.lm_title:focus, +input.lm_title +{ + max-width: unset !important; + transition-delay: unset; + width: 100%; + cursor: text; +} +input.lm_title { + transition-delay: 0.35s; + width: 100px; + cursor: pointer; +} +.tabDocView-drag { + margin: auto; +} +.miniMap-hidden, +.miniMap { + position: absolute; + overflow: hidden; + right: 10; + bottom: 10; + border: solid 1px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + width: 100%; + height: 100%; + transition: all 0.5s; + + .miniOverlay { + width: 100%; + height: 100%; + position: absolute; + + .miniThumb { + background: #25252525; + position: absolute; + } + } +} +.miniMap-hidden { + position: absolute; + bottom: 0; + right: 0; + width: 45px; + height: 45px; + background: white; + transform: translate(20px, 20px) rotate(45deg); + border-radius: 30px; + padding: 2px; + > svg { + margin-top: 3px; + transform: translate(0px, 7px); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx new file mode 100644 index 000000000..8c1a003b9 --- /dev/null +++ b/src/client/views/collections/TabDocView.tsx @@ -0,0 +1,384 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import 'golden-layout/src/css/goldenlayout-base.css'; +import 'golden-layout/src/css/goldenlayout-dark-theme.css'; +import { clamp } from 'lodash'; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import * as ReactDOM from 'react-dom'; +import { DataSym, Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Id } from '../../../fields/FieldSymbols'; +import { FieldId } from "../../../fields/RefField"; +import { listSpec } from '../../../fields/Schema'; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { PresBox, PresMovement } from '../nodes/PresBox'; +import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionViewType } from './CollectionView'; +import "./TabDocView.scss"; +import React = require("react"); +const _global = (window /* browser */ || global /* node */) as any; + +interface TabDocViewProps { + documentId: FieldId; + glContainer: any; +} +@observer +export class TabDocView extends React.Component<TabDocViewProps> { + _mainCont: HTMLDivElement | null = null; + _tabReaction: IReactionDisposer | undefined; + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _isActive: boolean = false; + @observable private _document: Doc | undefined; + @observable private _view: DocumentView | undefined; + + get stack(): any { return (this.props as any).glContainer.parent.parent; } + get tab() { return (this.props as any).glContainer.tab; } + get view() { return this._view; } + + @action + init = (tab: any, doc: Opt<Doc>) => { + if (tab.DashDoc !== doc && doc && tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + tab._disposers = {} as { [name: string]: IReactionDisposer }; + tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); + tab.DashDoc = doc; + CollectionDockingView.Instance.tabMap.add(tab); + + // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. + const titleEle = tab.titleElement[0]; + titleEle.size = StrCast(doc.title).length + 3; + titleEle.value = doc.title; + titleEle.onchange = (e: any) => { + titleEle.size = e.currentTarget.value.length + 3; + Doc.GetProto(doc).title = e.currentTarget.value; + }; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: MouseEvent) => { + if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + tab.header.parent.setActiveContentItem(tab.contentItem); + tab.setActive(true); + } + }; + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([dragHdl], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; + + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected + titleEle.onpointerdown = (e: any) => { + if (e.target.className !== "lm_close_tab" && this.view) { + SelectionManager.SelectDoc(this.view, false); + if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); + titleEle.lastClick = Date.now(); + (document.activeElement !== titleEle) && titleEle.focus(); + } + }; + tab._disposers.selectionDisposer = reaction(() => SelectionManager.SelectedDocuments().some(v => (v.topMost || v.props.treeViewDoc) && v.props.Document === doc), + (selected) => selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && + UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch")); + + //attach the selection doc buttons menu to the drag handle + const stack = tab.contentItem.parent; + const dragHdl = document.createElement("div"); + dragHdl.className = "lm_drag_tab"; + tab._disposers.buttonDisposer = reaction(() => this.view, view => + view && [ReactDOM.render(<span className="tabDocView-drag" onPointerDown={dragBtnDown}><CollectionDockingViewMenu views={() => [view]} Stack={stack} /></span>, dragHdl), tab._disposers.buttonDisposer?.()], + { fireImmediately: true }); + tab.reactComponents = [dragHdl]; + tab.closeElement.before(dragHdl); + + // highlight the tab when the tab document is brushed in any part of the UI + tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { + titleEle.value = title; + titleEle.style.padding = degree ? 0 : 2; + titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + }, { fireImmediately: true }); + + // clean up the tab when it is closed + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", doc, undefined, true, true); + SelectionManager.DeselectAll(); + tab.contentItem.remove(); + }); + } + } + + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static PinDoc(doc: Doc, unpin = false, audioRange?: boolean) { + if (unpin) console.log('remove unpin'); + //add this new doc to props.Document + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.title = doc.title; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (pinDoc.type === "audio" && !audioRange) { + pinDoc.presStartTime = 0; + pinDoc.presEndTime = doc.duration; + } + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + const curPresDocView = DocumentManager.Instance.getDocumentView(curPres); + if (!curPresDocView) { + CollectionDockingView.AddSplit(curPres, "right"); + } + DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); + } + } + + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static UnpinDoc(doc: Doc) { + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); + ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); + } + } + + componentDidMount() { + const selected = () => SelectionManager.SelectedDocuments().some(v => v.props.Document === this._document); + new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + })).observe(this.props.glContainer._element[0]); + this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(); + this._tabReaction = reaction(() => ({ selected: selected(), color: this.tabColor, title: this.tab?.titleElement[0] }), + ({ selected, color, title }) => title && (title.style.backgroundColor = selected ? color : ""), + { fireImmediately: true }); + } + + componentWillUnmount() { + this._tabReaction?.(); + this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); + } + + @action.bound + private onActiveContentItemChanged() { + if (this.props.glContainer.tab && this._isActive !== this.props.glContainer.tab.isActive) { + this._isActive = this.props.glContainer.tab.isActive; + (CollectionDockingView.Instance as any)._goldenLayout?.isInitialised && CollectionDockingView.Instance.stateChanged(); + !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. + } + } + + nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; + panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : + (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) + panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; + nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0; + contentScaling = () => { + const nativeH = NumCast(this.layoutDoc?._nativeHeight); + const nativeW = NumCast(this.layoutDoc?._nativeWidth); + let scaling = 1; + if (nativeW && (this.layoutDoc?._fitWidth || this._panelHeight / nativeH > this._panelWidth / nativeW)) { + scaling = this._panelWidth / nativeW; // width-limited or fitWidth + } else if (nativeW && nativeH) { + scaling = this._panelHeight / nativeH; // height-limited + } + return scaling; + } + + ScreenToLocalTransform = () => { + if (this._mainCont?.children) { + 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(); + } + @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}% ` : undefined; } + @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } + + // adds a tab to the layout based on the locaiton parameter which can be: + // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, + // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right + // replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right + // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack + addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { + SelectionManager.DeselectAll(); + const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":"); + const locationParams = locationFields.length > 1 ? locationFields[1] : ""; + switch (locationFields[0]) { + case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + case "close": return CollectionDockingView.CloseSplit(doc, locationParams); + case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); + case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); + case "inPlace": + case "add": + default: return CollectionDockingView.AddSplit(doc, locationParams, this.stack); + } + } + + @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document, 0))); } + @computed get renderBounds() { + const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + const xbounds = bounds[2] - bounds[0]; + const ybounds = bounds[3] - bounds[1]; + const dim = Math.max(xbounds, ybounds); + return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + } + childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + const doc = this._document; + const miniSize = this.returnMiniSize(); + doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * this.renderBounds.dim, this.renderBounds.l, this.renderBounds.l + this.renderBounds.dim); + doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * this.renderBounds.dim, this.renderBounds.t, this.renderBounds.t + this.renderBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + getCurrentFrame = () => { + return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); + } + renderMiniMap() { + const miniWidth = this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniHeight = this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniLeft = 50 + (NumCast(this._document?._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; + const miniTop = 50 + (NumCast(this._document?._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; + const miniSize = this.returnMiniSize(); + return <> + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.tabColor }}> + <CollectionFreeFormView + Document={this._document!} + LibraryPath={emptyPath} + CollectionView={undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. + noOverlay={true} // don't render overlay Docs since they won't scale + active={returnTrue} + select={emptyFunction} + dropAction={undefined} + isSelected={returnFalse} + dontRegisterView={true} + annotationsKey={""} + fieldKey={Doc.LayoutFieldKey(this._document!)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + ContentScaling={returnOne} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + fitToBox={true} + /> + <div className="miniOverlay" onPointerDown={this.miniDown} > + <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} /> + </div> + </div> + + <Tooltip title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> + </>; + } + focusFunc = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => void) => { + if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { + this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) + } + afterFocus?.(); + } + setView = action((view: DocumentView) => this._view = view); + active = () => this._isActive; + @computed get docView() { + TraceMobx(); + return !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : + <><DocumentView key={this._document[Id]} + LibraryPath={emptyPath} + Document={this._document} + getView={this.setView} + DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined} + NativeWidth={this.nativeWidth() ? this.nativeWidth : undefined} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + parentActive={this.active} + whenActiveChanged={emptyFunction} + focus={this.focusFunc} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + {this._document._viewType !== CollectionViewType.Freeform ? (null) : + <>{this._document.hideMinimap ? (null) : this.renderMiniMap()} + <Tooltip key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> + </>} + </>; + } + + render() { + return (<div className="collectionDockingView-content" ref={ref => { + if (this._mainCont = ref) { + (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); + DocServer.GetRefField(this.props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document))); + } + }} + style={{ + transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, + height: this.layoutDoc?._fitWidth ? undefined : "100%", + width: this.widthpercent + }}> + {this.docView} + </div >); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss new file mode 100644 index 000000000..17c6b0750 --- /dev/null +++ b/src/client/views/collections/TreeView.scss @@ -0,0 +1,115 @@ +@import "../globalCssVariables"; + +.treeView-container, +.treeView-container-active { + .bullet-outline { + position: relative; + width: 15px; + color: $intermediate-color; + transform: scale(0.5); + display: inline-block; + } + + .bullet { + position: relative; + width: 15px; + color: $intermediate-color; + margin-top: 3px; + transform: scale(1.3, 1.3); + border: #80808030 1px solid; + border-radius: 4px; + } +} +.treeView-container-active { + z-index: 100; + position: relative;; + .formattedTextbox-sidebar { + background-color: #ffff001f !important; + height: 500px !important; + } +} + +.treeView-openRight { + display: none; + height: 17px; + width: 15px; +} + +.treeView-openRight:hover { + background: #797777; + cursor: pointer; +} + +.treeView-border-outline, +.treeView-border { + display: flex; + overflow: hidden; +} +.treeView-border{ + border-left: dashed 1px #00000042; +} + +.treeView-header-editing, +.treeView-header { + border: transparent 1px solid; + display: flex; + //align-items: center; + ::-webkit-scrollbar { + display: none; + } + .formattedTextBox-cont { + .formattedTextbox-sidebar { + overflow: visible !important; + border-left: unset; + } + overflow: visible !important; + } + + .editableView-container-editing-oneLine { + min-width: 15px; + } + + .documentView-node-topmost { + width: unset; + } + + >svg { + display: none; + } + +} + +.treeView-header:hover { + .collectionTreeView-keyHeader { + display: inherit; + } + + >svg { + display: inherit; + } + + .treeView-openRight { + display: inline-block; + height: 17px; + width: 15px; + + // display: inline; + svg { + display: block; + padding: 0px; + margin-left: 3px; + } + } +} + +.treeView-header-above { + border-top: black 1px solid; +} + +.treeView-header-below { + border-bottom: black 1px solid; +} + +.treeView-header-inside { + border: black 1px solid; +}
\ No newline at end of file diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx new file mode 100644 index 000000000..1dfa5615a --- /dev/null +++ b/src/client/views/collections/TreeView.tsx @@ -0,0 +1,768 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { EditableView } from "../EditableView"; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import { DocumentView } from '../nodes/DocumentView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; +import { KeyValueBox } from '../nodes/KeyValueBox'; +import { CollectionTreeView } from './CollectionTreeView'; +import { CollectionView, CollectionViewType } from './CollectionView'; +import "./TreeView.scss"; +import React = require("react"); + +export interface TreeViewProps { + document: Doc; + dataDoc?: Doc; + containingCollection: Doc; + prevSibling?: Doc; + renderDepth: number; + removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; + moveDocument: DragManager.MoveFunction; + dropAction: dropActionType; + addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; + pinToPres: (document: Doc) => void; + panelWidth: () => number; + panelHeight: () => number; + ChromeHeight: undefined | (() => number); + addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; + indentDocument?: () => void; + outdentDocument?: () => void; + ScreenToLocalTransform: () => Transform; + dontRegisterView?: boolean; + backgroundColor?: (doc: Opt<Doc>, renderDepth: number) => string | undefined; + outerXf: () => { translateX: number, translateY: number }; + treeView: CollectionTreeView; + parentKey: string; + active: (outsideReaction?: boolean) => boolean; + treeViewHideHeaderFields: () => boolean; + treeViewPreventOpen: boolean; + renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; + ignoreFields?: string[]; + firstLevel: boolean; + whenActiveChanged: (isActive: boolean) => void; +} + +@observer +/** + * Renders a treeView of a collection of documents + * + * special fields: + * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden + * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree + */ +export class TreeView extends React.Component<TreeViewProps> { + private _editTitleScript: (() => ScriptField) | undefined; + private _openScript: (() => ScriptField) | undefined; + private _header?: React.RefObject<HTMLDivElement> = React.createRef(); + private _treedropDisposer?: DragManager.DragDropDisposer; + private _dref = React.createRef<HTMLDivElement>(); + private _tref = React.createRef<HTMLDivElement>(); + private _docRef = React.createRef<DocumentView>(); + private _uniqueId = Utils.GenerateGuid(); + private _editMaxWidth: number | string = 0; + + @computed get doc() { TraceMobx(); return this.props.document; } + get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } + get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive + get treeViewLockExpandedView() { return this.doc.treeViewLockExpandedView; } + get defaultExpandedView() { return StrCast(this.doc.treeViewDefaultExpandedView, this.noviceMode || this.outlineMode ? "layout" : "fields"); } + get treeViewDefaultExpandedView() { return this.treeViewLockExpandedView ? this.defaultExpandedView : (this.childDocs ? this.fieldKey : this.defaultExpandedView); } + @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.treeViewPreventOpen) this._overrideTreeViewOpen = c; + else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; + } + @computed get outlineMode() { return this.props.treeView.doc.treeViewOutlineMode; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } + @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.treeViewDefaultExpandedView); } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } + @computed get dataDoc() { return this.doc[DataSym]; } + @computed get layoutDoc() { return Doc.Layout(this.doc); } + @computed get fieldKey() { TraceMobx(); const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } + childDocList(field: string) { + const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; + return ((this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field + (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields + DocListCastOrNull(this.doc[field])); // otherwise use the document's data field + } + @computed get childDocs() { TraceMobx(); return this.childDocList(this.fieldKey); } + @computed get childLinks() { return this.childDocList("links"); } + @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } + @computed get boundsOfCollectionDocument() { + 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 openRight = () => this.props.addDocTab(this.doc, "add:right"); + @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { + return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc); + } + @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { + this.props.treeView.props.select(false); + const ind = this.dataDoc[key].indexOf(doc); + const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); + res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.CollectionView)?.select(false); + return res; + } + @undoBatch @action removeDoc = (doc: Doc | Doc[]) => this.remove(doc, Doc.LayoutFieldKey(this.doc)); + + constructor(props: any) { + super(props); + const titleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); + const openScript = ScriptField.MakeScript(`openOnRight(self)`); + const treeOpenScript = ScriptField.MakeScript(`self.treeViewOpen = !self.treeViewOpen`); + this._editTitleScript = !Doc.IsSystem(this.props.document) ? titleScript && (() => titleScript) : treeOpenScript && (() => treeOpenScript); + this._openScript = !Doc.IsSystem(this.props.document) ? openScript && (() => openScript) : undefined; + if (Doc.GetT(this.props.document, "editTitle", "string", true) === "*") Doc.SetInPlace(this.props.document, "editTitle", this._uniqueId, false); + } + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer?.(); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); + } + + componentWillUnmount() { + document.removeEventListener("pointermove", this.onDragMove, true); + document.removeEventListener("pointermove", this.onDragUp, true); + } + + onDragUp = (e: PointerEvent) => { + document.removeEventListener("pointerup", this.onDragUp, true); + document.removeEventListener("pointermove", this.onDragMove, true); + } + onPointerEnter = (e: React.PointerEvent): void => { + this.props.active(true) && Doc.BrushDoc(this.dataDoc); + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + this._header!.current!.className = "treeView-header"; + document.removeEventListener("pointermove", this.onDragMove, true); + document.addEventListener("pointermove", this.onDragMove, true); + document.removeEventListener("pointerup", this.onDragUp, true); + document.addEventListener("pointerup", this.onDragUp, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + Doc.UnBrushDoc(this.dataDoc); + if (this._header?.current?.className !== "treeView-header-editing") { + this._header!.current!.className = "treeView-header"; + } + document.removeEventListener("pointerup", this.onDragUp, true); + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + Doc.UnBrushDoc(this.dataDoc); + 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 && this.childDocList.length); + this._header!.current!.className = "treeView-header"; + if (inside) this._header!.current!.className += " treeView-header-inside"; + else if (before) this._header!.current!.className += " treeView-header-above"; + else if (!before) this._header!.current!.className += " treeView-header-below"; + e.stopPropagation(); + } + + public static makeTextBullet() { + const bullet = Docs.Create.TextDocument("-text-", { title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, forceActive: true, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); + Doc.GetProto(bullet).layout = CollectionView.LayoutString("data"); + Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); + Doc.GetProto(bullet).data = new List<Doc>([]); + Doc.SetInPlace(bullet, "editTitle", "*", false); + FormattedTextBox.SelectOnLoad = bullet[Id]; + return bullet; + } + + makeTextCollection = () => { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + const bullet = TreeView.makeTextBullet(); + const added = this.props.addDocument(bullet); + bullet.context = this.props.treeView.Document; + return added; + } + + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline-block"} + editing={true} + contents={StrCast(this.doc[key])} + height={12} + sizeToContent={true} + fontStyle={style} + fontSize={12} + GetValue={() => StrCast(this.doc[key])} + SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => { + Doc.SetInPlace(this.doc, key, value, false); + if (this.outlineMode && enterKey) { + this.makeTextCollection(); + } else { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + } + })} + onClick={() => { + SelectionManager.DeselectAll(); + return false; + }} + OnEmpty={undoBatch(() => this.props.treeView.doc.treeViewOutlineMode && this.props.removeDoc?.(this.doc))} + OnTab={undoBatch((shift?: boolean) => { + shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); + setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", `${this.props.treeView._uniqueId}`, false), 0); + })} + />) + + preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + const dragData = de.complete.docDragData; + dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? "same" : dragData.dropAction); + } + + @undoBatch + treeDrop = (e: Event, de: DragManager.DropEvent) => { + 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 && this.childDocList.length); + if (de.complete.linkDragData) { + const sourceDoc = de.complete.linkDragData.linkSourceDocument; + const destDoc = this.doc; + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); + e.stopPropagation(); + } + const docDragData = de.complete.docDragData; + if (docDragData) { + e.stopPropagation(); + if (docDragData.draggedDocuments[0] === this.doc) return true; + const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + let addDoc = parentAddDoc; + if (inside) { + const localAdd = (doc: Doc) => { + const added = Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); + added && (doc.context = this.doc.context); + return added; + }; + addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( + (flg: boolean, doc) => flg && localAdd(doc), true) || parentAddDoc(doc); + } + const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; + return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); + } + return false; + } + + refTransform = (ref: HTMLDivElement) => { + const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); + const outerXf = this.props.outerXf(); + const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + } + docTransform = () => this.refTransform(this._dref.current!); + getTransform = () => this.refTransform(this._tref.current!); + docWidth = () => { + const layoutDoc = this.layoutDoc; + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + } + docHeight = () => { + const layoutDoc = this.layoutDoc; + const bounds = this.boundsOfCollectionDocument; + return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return this.docWidth() * aspect; + if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); + return layoutDoc._fitWidth ? (!this.doc._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; + })())); + } + + @computed get expandedField() { + const ids: { [key: string]: string } = {}; + const rows: JSX.Element[] = []; + const doc = this.doc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + + for (const key of Object.keys(ids).slice().sort()) { + if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; + const contents = doc[key]; + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + + if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); + const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { + const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); + added && (doc.context = this.doc.context); + return added; + }; + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); + contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), + this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView); + } else { + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={12} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; + } + rows.push(<div style={{ display: "flex" }} key={key}> + <span style={{ fontWeight: "bold" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + rows.push(<div style={{ display: "flex" }} key={"newKeyValue"}> + <EditableView + key="editableView" + contents={"+key:value"} + height={13} + fontSize={12} + GetValue={returnEmptyString} + SetValue={value => value.indexOf(":") !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(":")), value.substring(value.indexOf(":") + 1, value.length), true)} /> + </div>); + return rows; + } + + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); + rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), 20); + + @computed get renderContent() { + TraceMobx(); + const expandKey = this.treeViewExpandedView; + if (["links", "annotations", this.fieldKey].includes(expandKey)) { + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); + const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { + const added = Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); + added && (doc.context = this.doc.context); + return added; + }; + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); + const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; + const sortKey = `${this.fieldKey}-sortAscending`; + return <ul key={expandKey + "more"} className={this.doc.treeViewHideTitle ? "no-indent" : ""} onClick={(e) => { + !this.outlineMode && (this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true))); + e.stopPropagation(); + }}> + {!docs ? (null) : + TreeView.GetChildElements(docs, this.props.treeView, this.layoutDoc, + this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView)} + </ul >; + } else if (this.treeViewExpandedView === "fields") { + return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > + {this.expandedField} + </div></ul>; + } else { + const layoutDoc = this.layoutDoc; + const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; + const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; + return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LibraryPath={emptyPath} + dontRegisterView={this.props.dontRegisterView} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + backgroundColor={this.props.backgroundColor} + fitToBox={this.boundsOfCollectionDocument !== undefined} + FreezeDimensions={true} + NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} + NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} + PanelWidth={panelWidth} + PanelHeight={panelHeight} + focus={returnFalse} + ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionView={undefined} + addDocument={returnFalse} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div>; + } + } + + get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } + + @action + bulletClick = (e: React.MouseEvent) => { + if (this.onCheckedClick) { + this.onCheckedClick?.script.run({ + this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, + heading: this.props.containingCollection.title, + checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeView.props.Document, + }, console.log); + } else { + this.treeViewOpen = !this.treeViewOpen; + } + e.stopPropagation(); + } + + @computed get renderBullet() { + TraceMobx(); + const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; + return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} + onClick={this.bulletClick} + style={this.outlineMode ? { opacity: NumCast(this.doc.opacity, 1) } : { + color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), + opacity: checked === "unchecked" ? undefined : 0.4 + }}> + {this.outlineMode && !(this.doc.text as RichTextField)?.Text ? (null) : + <FontAwesomeIcon icon={this.outlineMode ? [this.childDocs?.length && !this.treeViewOpen ? "fas" : "far", "circle"] : + checked === "check" ? "check" : + (checked === "x" ? "times" : checked === "unchecked" ? "square" : + !this.treeViewOpen ? (this.childDocs?.length ? "caret-square-right" : "caret-right") : + (this.childDocs?.length ? "caret-square-down" : "caret-down"))} />} + </div>; + } + @computed get showTitleEditorControl() { return ["*", this._uniqueId, this.props.treeView._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); } + @computed get headerElements() { + return (Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode) || this.props.treeViewHideHeaderFields() ? (null) : + <> + <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> + <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} + onPointerDown={action(() => { + if (this.treeViewOpen) { + this.doc.treeViewExpandedView = this.treeViewLockExpandedView ? this.doc.treeViewExpandedView : + this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields") : + this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : + this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : + (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : + this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields"); + } + this.treeViewOpen = true; + })}> + {this.treeViewExpandedView} + </span> + </>; + } + + showContextMenu = (e: React.MouseEvent) => simulateMouseClick(this._docRef.current?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + contextMenuItems = () => Doc.IsSystem(this.doc) ? [] : [{ script: ScriptField.MakeFunction(`openOnRight(self)`)!, label: "Open" }, { script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; + truncateTitleWidth = () => NumCast(this.props.treeView.props.Document.treeViewTruncateTitleWidth, 0); + onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); + onChildDoubleClick = () => (!this.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); + /** + * Renders the EditableView title element for placement into the tree. + */ + @computed + get renderTitle() { + TraceMobx(); + const view = this.showTitleEditorControl ? this.editableView("title") : + <DocumentView + ref={this._docRef} + Document={this.doc} + DataDoc={undefined} + treeViewDoc={this.props.treeView.props.Document} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.props.addDocTab} + rootSelected={returnTrue} + pinToPres={emptyFunction} + onClick={this.onChildClick} + onDoubleClick={this.onChildDoubleClick} + dropAction={this.props.dropAction} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + ScreenToLocalTransform={this.getTransform} + ContentScaling={returnOne} + PanelWidth={this.truncateTitleWidth} + PanelHeight={returnZero} + contextMenuItems={this.contextMenuItems} + opacity={this.outlineMode ? undefined : returnOne} + renderDepth={1} + focus={returnTrue} + parentActive={returnTrue} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={emptyFunction} + dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={this.props.containingCollection} + />; + return <> + <div className={`docContainer${Doc.IsSystem(this.props.document) ? "-system" : ""}`} ref={this._tref} title="click to edit title. Double Click or Drag to Open" + style={{ + fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? "bold" : undefined, + textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, + outline: this.doc === CurrentUserUtils.ActiveDashboard ? "dashed 1px #06123232" : undefined, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined + }} > + {view} + </div > + {this.headerElements} + </>; + } + + refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); + + render() { + TraceMobx(); + if (this.props.renderedIds.indexOf(this.doc[Id]) !== -1) return null; + const sorting = this.doc[`${this.fieldKey}-sortAscending`]; + if (this.showTitleEditorControl) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll + let par: any = this._header?.current; + while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; + if (par) { + const par_rect = (par as HTMLElement).getBoundingClientRect(); + const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect(); + this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0)); + } + } + else this._editMaxWidth = ""; + const selected = SelectionManager.IsSelected(DocumentManager.Instance.getFirstDocumentView(this.doc)); + return this.doc.treeViewHideHeader || this.outlineMode ? + !StrCast(Doc.LayoutField(this.doc)).includes("CollectionView") ? + this.renderContent + : <div className={`treeView-container${selected ? "-active" : ""}`} ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()} + onKeyDown={e => { + e.stopPropagation(); + e.preventDefault(); + switch (e.key) { + case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc); + case "Enter": return UndoManager.RunInBatch(() => this.makeTextCollection(), "bullet"); + case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150); + return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.() : this.props.indentDocument?.(), "tab"); + } + }} > + <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ alignItems: this.outlineMode ? "center" : undefined, maxWidth: this._editMaxWidth }} + onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} + onPointerDown={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet} + <div ref={this._dref} style={{ display: "inline-block", height: this.rtfOutlineHeight() }} key={this.doc[Id]}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LayoutTemplateString={FormattedTextBox.LayoutString("text")} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + backgroundColor={this.props.backgroundColor} + fitToBox={this.boundsOfCollectionDocument !== undefined} + PanelWidth={this.rtfWidth} + PanelHeight={this.rtfOutlineHeight} + focus={this.refocus} + ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionView={undefined} + addDocument={this.props.addDocument} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div> + </div> + + <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> + {!this.treeViewOpen ? (null) : this.renderContent} + </div> + </div> : + <div className="treeView-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> + <li className="collection-child"> + <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + SelectionManager.DeselectAll(); + } + }} + onPointerDown={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet} + {this.renderTitle} + </div> + <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> + {!this.treeViewOpen ? (null) : this.renderContent} + </div> + </li> + </div>; + } + + + public static GetChildElements( + childDocs: Doc[], + treeView: CollectionTreeView, + containingCollection: Doc, + dataDoc: Doc | undefined, + key: string, + parentCollectionDoc: Doc | undefined, + parentPrevSibling: Doc | undefined, + add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, + remove: ((doc: Doc | Doc[]) => boolean), + move: DragManager.MoveFunction, + dropAction: dropActionType, + addDocTab: (doc: Doc, where: string) => boolean, + pinToPres: (document: Doc) => void, + backgroundColor: undefined | ((document: Opt<Doc>, renderDepth: number) => string | undefined), + screenToLocalXf: () => Transform, + outerXf: () => { translateX: number, translateY: number }, + active: (outsideReaction?: boolean) => boolean, + panelWidth: () => number, + ChromeHeight: undefined | (() => number), + renderDepth: number, + treeViewHideHeaderFields: () => boolean, + treeViewPreventOpen: boolean, + renderedIds: string[], + onCheckedClick: undefined | (() => ScriptField), + onChildClick: undefined | (() => ScriptField), + ignoreFields: string[] | undefined, + firstLevel: boolean, + whenActiveChanged: (isActive: boolean) => void, + dontRegisterView: boolean | undefined) { + const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); + if (viewSpecScript) { + childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); + } + + const docs = childDocs.slice(); + const ascending = containingCollection?.[key + "-sortAscending"]; + if (ascending !== undefined) { + const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { + const reN = /[0-9]*$/; + const aA = a.replace(reN, ""); // get rid of trailing numbers + 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 first = (ascending ? b : a).title; + const second = (ascending ? a : b).title; + if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1; + if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second); + return ascending ? 1 : -1; + }); + } + + const rowWidth = () => panelWidth() - 20; + return docs.filter(child => child instanceof Doc).map((child, i) => { + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); + if (!pair.layout || pair.data instanceof Promise) { + return (null); + } + + const indent = i === 0 ? undefined : () => { + if (remove && 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) { + remove(child); + FormattedTextBox.SelectOnLoad = child[Id]; + Doc.AddDocToList(docs[i - 1], fieldKey, child); + docs[i - 1].treeViewOpen = true; + child.context = treeView.Document; + } + } + }; + const outdent = !parentCollectionDoc ? undefined : () => { + if (parentCollectionDoc._viewType === CollectionViewType.Tree && remove && StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; + remove(child); + FormattedTextBox.SelectOnLoad = child[Id]; + Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); + parentCollectionDoc.treeViewOpen = true; + child.context = treeView.Document; + } + }; + const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); + const childLayout = Doc.Layout(pair.layout); + const rowHeight = () => { + const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); + return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + }; + return <TreeView key={child[Id]} + document={pair.layout} + dataDoc={pair.data} + containingCollection={containingCollection} + prevSibling={docs[i]} + treeView={treeView} + indentDocument={indent} + outdentDocument={!parentCollectionDoc ? undefined : outdent} + onCheckedClick={onCheckedClick} + onChildClick={onChildClick} + renderDepth={renderDepth} + removeDoc={StrCast(containingCollection.freezeChildren).includes("remove") ? undefined : remove} + addDocument={addDocument} + backgroundColor={backgroundColor} + panelWidth={rowWidth} + panelHeight={rowHeight} + ChromeHeight={ChromeHeight} + dontRegisterView={dontRegisterView} + moveDocument={move} + dropAction={dropAction} + addDocTab={addDocTab} + pinToPres={pinToPres} + ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} + parentKey={key} + active={active} + treeViewHideHeaderFields={treeViewHideHeaderFields} + treeViewPreventOpen={treeViewPreventOpen} + renderedIds={renderedIds} + ignoreFields={ignoreFields} + firstLevel={firstLevel} + whenActiveChanged={whenActiveChanged} />; + }); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index b00074cc6..bc2cb2d20 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -10,6 +10,7 @@ import { Id, ToString } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { RefField } from "../../../../fields/RefField"; import { listSpec } from "../../../../fields/Schema"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export interface ViewDefBounds { type: string; @@ -359,7 +360,7 @@ export function computeTimelineLayout( groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 3a2979696..9dcfde7f9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -26,7 +26,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this._anchorDisposer?.(); } @action - timeout = () => (Date.now() < this._start++ + 1000) && setTimeout(this.timeout, 25) + timeout = action(() => (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { 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(() => { @@ -103,28 +103,67 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo // showLinks={action(() => { })} // />, { x: 300, y: 300 }); }); + + + } + + visibleY = (el: any) => { + let rect = el.getBoundingClientRect(); + const top = rect.top, height = rect.height; + var el = el.parentNode; + do { + rect = el.getBoundingClientRect(); + if (top <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; + // Check if the element is out of view due to a container scrolling + if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; + el = el.parentNode; + } while (el !== document.body); + // Check its within the document viewport + return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; + } + visibleX = (el: any) => { + let rect = el.getBoundingClientRect(); + const left = rect.left, width = rect.width; + var el = el.parentNode; + do { + rect = el.getBoundingClientRect(); + if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right; + // Check if the element is out of view due to a container scrolling + if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left; + el = el.parentNode; + } while (el !== document.body); + // Check its within the document viewport + return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } - @computed get renderData() { - this._start; - if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { + @computed.struct get renderData() { + this._start; SnappingManager.GetIsDragging(); + if (!this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { return undefined; } this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); const acont = this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const bcont = this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const a = (acont.length ? acont[0] : this.props.A.ContentDiv).getBoundingClientRect(); - const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv).getBoundingClientRect(); - const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, - b.left, b.top, b.width, b.height, + const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); + for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; + for (let apdiv = bdiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; + const a = adiv.getBoundingClientRect(); + const b = bdiv.getBoundingClientRect(); + const atop = this.visibleY(adiv); + const btop = this.visibleY(bdiv); + const aleft = this.visibleX(adiv); + const bleft = this.visibleX(bdiv); + const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, + bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height, - a.left, a.top, a.width, a.height, + const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, + aleft, atop, a.width, a.height, apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - const pt1vec = [pt1[0] - (a.left + a.width / 2), pt1[1] - (a.top + a.height / 2)]; - const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + b.height / 2)]; + const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)]; + const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; @@ -132,6 +171,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document); + if (aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top) return { a, b, pt1norm: [0, 0], pt2norm: [0, 0], aActive, bActive, textX: undefined, textY: undefined, pt1, pt2 }; const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetY); @@ -144,9 +184,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> - <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > + {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {StrCast(this.props.LinkDocs[0].description)} - </text> + </text>} </>); } }
\ 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 1a2421bfd..6a1a41ca7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed } from "mobx"; +import { computed, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; @@ -10,12 +10,10 @@ import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; import { DocumentType } from "../../../documents/DocumentTypes"; import { SnappingManager } from "../../../util/SnappingManager"; -import { Cast } from "../../../../fields/Types"; @observer export class CollectionFreeFormLinksView extends React.Component { - @computed - get uniqueConnections() { + @computed get uniqueConnections() { const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { if (!drawnPairs.reduce((found, drawnPair) => { const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); @@ -37,7 +35,7 @@ export class CollectionFreeFormLinksView extends React.Component { } render() { - return SnappingManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container"> + return <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} </svg> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 2b07c4efb..75cbc20ca 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -52,12 +52,16 @@ .pathOrder-frame { position: absolute; - width: 40; + width: 100%; + height: 100%; + min-height: 15px; text-align: center; - font-size: 30; background-color: #69a6db; + border-radius: 5px; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); font-family: Roboto; - font-weight: 300; + font-weight: 500; + color: white; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 46e30f616..2783011cf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,4 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; @@ -15,7 +12,7 @@ import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, Utils, setupMoveUpEvents } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -24,10 +21,12 @@ import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; +import { LinkManager } from "../../../util/LinkManager"; +import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; @@ -37,29 +36,25 @@ import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; +import { PresBox } from "../../nodes/PresBox"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionViewType } from "../CollectionView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { PresBox } from "../../nodes/PresBox"; -import { SearchUtil } from "../../../util/SearchUtil"; -import { LinkManager } from "../../../util/LinkManager"; - -library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", - currentTimecode: "number", + _currentTimecode: "number", displayTimecode: "number", - currentFrame: "number", + _currentFrame: "number", arrangeInit: ScriptField, - useClusters: "boolean", + _useClusters: "boolean", fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set @@ -109,28 +104,32 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable canPanY: boolean = true; @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } - @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 fitToContent() { return this.props.fitToBox || (this.Document._fitToBox && !this.isAnnotationOverlay); } + @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } - @computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); } - @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); } + @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); } + @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - 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 = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? - Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), - this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : - NumCast(this.Document[this.scaleFieldKey], 1)) - + private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; + private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; + private zoomScaling = () => { + const mult = this.fitToContentScaling / this.parentScaling; + if (this.fitToContent) { + const zs = !this.childDocs.length ? 1 : + Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); + return mult * (this.props.isAnnotationOverlay ? Math.min(zs, 1) : zs); + } + return mult * NumCast(this.Document[this.scaleFieldKey], 1); + } @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -178,8 +177,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newBox.y = y; } } - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { - CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentFrame); + if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { + CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document._currentFrame, true); } } return retVal; @@ -189,7 +188,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } - public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -212,9 +211,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined) { + if (this.Document._currentFrame !== undefined) { const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, vals.opacity); + CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, this.Document.editScrollProgressivize ? vals.scroll : undefined, vals.opacity); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; @@ -222,7 +221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); - d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront + !d._isBackground && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); @@ -261,7 +260,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - // if (this.props.Document.isBackground) return false; + // if (this.props.Document._isBackground) return false; const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) { return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); @@ -306,8 +305,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action - updateClusters(useClusters: boolean) { - this.props.Document.useClusters = useClusters; + updateClusters(_useClusters: boolean) { + this.props.Document._useClusters = _useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -315,7 +314,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + 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); @@ -325,7 +324,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docFirst.cluster = i; } }))); - if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { docFirst.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -338,7 +337,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); - } else { + } else if (this._clusterSets.length) { 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)); } @@ -350,7 +349,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + if (this.props.Document._useClusters) { this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); const preferredInd = NumCast(doc.cluster); doc.cluster = -1; @@ -359,7 +358,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = i; } })); - if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -370,27 +369,27 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (doc.cluster === -1) { doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); - } else { + } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); this._clusterSets[doc.cluster].push(doc); } } } - getClusterColor = (doc: Doc) => { + getClusterColor = (doc: Opt<Doc>) => { let clusterColor = this.props.backgroundColor?.(doc, this.props.renderDepth + 1); - const cluster = NumCast(doc.cluster); - if (this.Document.useClusters) { + const cluster = NumCast(doc?.cluster); + if (this.Document._useClusters) { if (this._clusterSets.length <= cluster) { - setTimeout(() => this.updateCluster(doc), 0); + setTimeout(() => doc && this.updateCluster(doc), 0); } else { // choose a cluster color from a palette const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; clusterColor = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); - set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => !s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); } } return clusterColor; @@ -402,7 +401,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { return; } - this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; + this._hitCluster = this.props.Document._useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { @@ -591,7 +590,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { - runInAction(() => DocumentLinksButton.StartLink = undefined); + runInAction(() => { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + }); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); @@ -605,7 +607,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 - // PDFMenu.Instance.fadeOut(true); (commented out for mobile) const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); @@ -641,7 +642,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { - // panning a workspace if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; @@ -780,9 +780,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; - if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling(); - } if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); @@ -796,7 +793,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay) return; + if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay || this.props.Document.treeViewOutlineMode) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -843,14 +840,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } bringToFront = action((doc: Doc, sendToBack?: boolean) => { - if (sendToBack || doc.isBackground) { + if (sendToBack || doc._isBackground) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; } else { const docs = this.childLayoutPairs.map(pair => pair.layout); docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zlast = docs.length ? NumCast(docs[docs.length - 1].zIndex) : 1; + let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; zlast = docs.length + 1; @@ -908,19 +905,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; - // if (!willZoom && DocumentView._focusHack.length) { - // Doc.BrushDoc(this.props.Document); - // !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); - // } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... - if (!doc.z) this.setPan(newPanX, newPanY, doc.presTransition || doc.presTransition === 0 ? `transform ${doc.presTransition}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // 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?.()) { @@ -935,15 +927,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { - this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); + const pw = this.props.PanelWidth(); + const ph = this.props.PanelHeight(); + pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } + @computed get backgroundActive() { return this.layoutDoc._isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); - backgroundHalo = () => BoolCast(this.Document.useClusters); - parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; + backgroundHalo = () => BoolCast(this.Document._useClusters); + parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { addDocument: this.props.addDocument, @@ -951,8 +945,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P moveDocument: this.props.moveDocument, pinToPres: this.props.pinToPres, whenActiveChanged: this.props.whenActiveChanged, - NativeHeight: returnZero, - NativeWidth: returnZero, fitToBox: false, DataDoc: childData, Document: childLayout, @@ -960,7 +952,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, - layoutKey: undefined, + layoutKey: StrCast(this.props.Document.childLayoutKey), setupDragLines: this.setupDragLines, dontRegisterView: this.props.dontRegisterView, rootSelected: childData ? this.rootSelected : returnFalse, @@ -975,6 +967,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, + docRangeFilters: this.docRangeFilters, + searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, backgroundColor: this.getClusterColor, backgroundHalo: this.backgroundHalo, @@ -1008,8 +1002,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); - const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout : - CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0); + const { x, y, opacity } = this.Document._currentFrame === undefined ? params.pair.layout : + CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), @@ -1045,7 +1039,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else if (viewDef.type === "div") { return [x, y].some(val => val === undefined) ? undefined : { - ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)} + ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z + viewDef.payload} onClick={e => this.onViewDefDivClick(e, viewDef)} style={{ width, height, backgroundColor: color, transform }} />, bounds: viewDef }; @@ -1115,7 +1109,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); - const elements: ViewDefResult[] = computedElementData.slice(); + const elements = computedElementData.slice(); const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => elements.push({ @@ -1126,8 +1120,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} pointerEvents={this.backgroundActive || this.props.childPointerEvents ? - true : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined} + "all" : + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this @@ -1136,11 +1130,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); - if (this.props.isAnnotationOverlay) { - this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); + if (this.props.isAnnotationOverlay) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar + if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); + else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } - this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); + this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1175,7 +1170,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (this._marqueeRef?.current) { + if (!this.props.Document._noAutoscroll && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); @@ -1226,7 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); } @undoBatch @@ -1238,11 +1233,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private thumbIdentifier?: number; onContextMenu = (e: React.MouseEvent) => { - if (this.props.annotationsKey || !ContextMenu.Instance) return; + if (this.props.annotationsKey || this.props.Document.annotationOn || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); + !Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); @@ -1252,17 +1248,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; - !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }) : null; + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && - optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); + optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: "eye" }); this.props.ContainingCollectionView && - optionItems.push({ description: "Undo Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: "Move Items Out of Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); - optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); @@ -1271,7 +1267,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; - moreItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); + moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } @@ -1315,7 +1312,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @action - setupDragLines = () => { + setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); if (activeDocs.length > 50) { DragManager.SetSnapLines([], []); @@ -1331,13 +1328,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }; const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to; const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; - this.getActiveDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to + this.getActiveDocuments().filter(doc => !doc._isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs const horizLines: number[] = []; const vertLines: number[] = []; - snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { + snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); const docSize = this.getTransform().inverse().transformDirection(width, height); @@ -1349,7 +1346,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onPointerOver = (e: React.PointerEvent) => { if (SnappingManager.GetIsDragging()) { - this.setupDragLines(); + this.setupDragLines(e.ctrlKey || e.shiftKey); } e.stopPropagation(); } @@ -1379,7 +1376,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P _nudgeTime = 0; nudge = action((x: number, y: number) => { - if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // bcz: this isn't ideal, but want to try it out... + if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || + this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); this._nudgeTime = Date.now(); @@ -1391,7 +1389,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @computed get marqueeView() { return <MarqueeView {...this.props} - nudge={this.nudge} + nudge={this.isAnnotationOverlay ? undefined : this.nudge} addDocTab={this.addDocTab} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} @@ -1406,25 +1404,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P centeringShiftY={this.centeringShiftY} presPaths={BoolCast(this.Document.presPathView)} progressivize={BoolCast(this.Document.editProgressivize)} - zoomProgressivize={BoolCast(this.Document.editZoomProgressivize)} + presPinView={BoolCast(this.Document.presPinView)} transition={Cast(this.layoutDoc._viewTransition, "string", null)} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} - </CollectionFreeFormViewPannableContents></div> + </CollectionFreeFormViewPannableContents> + </div> {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @computed get contentScaling() { if (this.props.annotationsKey && !this.props.forceScaling) return 0; - const nw = NumCast(this.Document._nativeWidth, this.props.NativeWidth()); - const nh = NumCast(this.Document._nativeHeight, this.props.NativeHeight()); + const nw = returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); + const nh = returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - @computed get backgroundEvents() { return this.layoutDoc.isBackground && SnappingManager.GetIsDragging(); } + @computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); } render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1492,7 +1491,7 @@ interface CollectionFreeFormViewPannableContentsProps { transition?: string; presPaths?: boolean; progressivize?: boolean; - zoomProgressivize?: boolean; + presPinView?: boolean; } @observer @@ -1504,11 +1503,9 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF e.stopPropagation(); e.preventDefault(); const corner = e.target as any; - console.log(corner.id); if (corner) this._drag = corner.id; const rect = document.getElementById(this._drag); if (rect) { - console.log(this._drag); setupMoveUpEvents(e.target, e, this.onPointerMove, (e) => { }, (e) => { }); } } @@ -1559,48 +1556,25 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF doc.style.top = toNumber(top, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; } - this.updateAll(height, width, top, left); + // this.updateAll(height, width, top, left); return false; } return true; } - @action - updateAll = (width: number, height: number, top: number, left: number) => { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - this.updateList(targetDoc, activeItem["viewfinder-width-indexed"], width); - this.updateList(targetDoc, activeItem["viewfinder-height-indexed"], height); - this.updateList(targetDoc, activeItem["viewfinder-top-indexed"], top); - this.updateList(targetDoc, activeItem["viewfinder-left-indexed"], left); - } - - @action - updateList = (doc: Doc, list: any, val: number) => { - const x: List<number> = list; - if (x && x.length >= NumCast(doc.currentFrame) + 1) { - x[NumCast(doc.currentFrame)] = val; - list = x; - } else if (doc && x) { - x.length = NumCast(doc.currentFrame) + 1; - x[NumCast(doc.currentFrame)] = val; - list = x; - } - } - // scale: NumCast(targetDoc._viewScale), @computed get zoomProgressivizeContainer() { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - if (activeItem && activeItem.zoomProgressivize) { - const vfLeft: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); - const vfWidth: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-width-indexed"]); - const vfTop: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-top-indexed"]); - const vfHeight: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-height-indexed"]); + const activeItem = PresBox.Instance.activeItem; + // const targetDoc = PresBox.Instance.targetDoc; + if (activeItem && activeItem.presPinView && activeItem.id) { + const vfLeft: number = NumCast(activeItem.presPinViewX); + const vfTop: number = NumCast(activeItem.presPinViewY); + const vfWidth: number = 100; + const vfHeight: number = 100; return ( <> - {!activeItem.editZoomProgressivize ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> - <div className='resizers'> + {!this.props.presPinView ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> + <div className='resizers' key={'resizer' + activeItem.id}> <div id="resizer-tl" className='resizer top-left' onPointerDown={this.onPointerDown}></div> <div id="resizer-tr" className='resizer top-right' onPointerDown={this.onPointerDown}></div> <div id="resizer-bl" className='resizer bottom-left' onPointerDown={this.onPointerDown}></div> @@ -1613,7 +1587,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF } @computed get zoomProgressivize() { - return PresBox.Instance && this.props.zoomProgressivize ? this.zoomProgressivizeContainer : (null); + return PresBox.Instance && PresBox.Instance.activeItem && PresBox.Instance.activeItem.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); } @computed get progressivize() { @@ -1622,30 +1596,26 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF @computed get presPaths() { const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden"); - return !(PresBox.Instance) ? (null) : (<> - {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div> - <svg className={presPaths}> - <defs> - <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" /> - </marker> - <marker id="square" markerWidth="3" markerHeight="3" overflow="visible" - refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" /> - </marker> - <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4" - orient="auto" overflow="visible"> - <rect x="1" y="1" width="5" height="5" fill="#69a6db" /> - </marker> - - <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7" - orient="auto" overflow="visible"> - <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" /> - </marker> - </defs>; - {PresBox.Instance.paths} - </svg></>} - </>); + return !PresBox.Instance || !this.props.presPaths ? (null) : <> + <div key="presorder">{PresBox.Instance.order}</div> + <svg key="svg" className={presPaths}> + <defs> + <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" + orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" + orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> + </marker> + <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" + orient="auto" overflow="visible"> + <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + </defs> + {PresBox.Instance.paths} + </svg> + </>; } render() { @@ -1657,9 +1627,16 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); return <div className={freeformclass} + onScroll={e => { + const target = e.target as any; + if (getComputedStyle(target)?.overflow === "visible") { // if collection is visible, then scrolling will mess things up since there are no scroll bars + target.scrollTop = target.scrollLeft = 0; + } + }} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, - transition: this.props.transition + transition: this.props.transition, + //willChange: "transform" }}> {this.props.children()} {this.presPaths} @@ -1667,4 +1644,4 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF {this.zoomProgressivize} </div>; } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss deleted file mode 100644 index d49ab27fb..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ /dev/null @@ -1,68 +0,0 @@ -.antimodeMenu-button { - width: 200px; - position: relative; - text-align: left; - - .color-previewI { - width: 100%; - height: 40%; - } - - .color-previewII { - width: 100%; - height: 100%; - } -} - -.antimenu-Buttonup { - position: absolute; - width: 20; - height: 10; - right: 0; - padding: 0; -} - -.formatShapePane-inputBtn { - width: inherit; - position: absolute; -} - -.btn-group-palette { - .sketch-picker { - background: #323232; - width: 160px !important; - height: 80% !important; - - .flexbox-fit { - background: #323232; - } - } -} - -.btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - /* Make the buttons appear below each other */ -} - -.btn-group-palette { - display: block; - /* Make the buttons appear below each other */ -} - -.btn-draw { - display: inline; - /* Make the buttons appear below each other */ -} - -.btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx deleted file mode 100644 index a7f44bbbf..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx +++ /dev/null @@ -1,558 +0,0 @@ -import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field, Opt } from "../../../../fields/Doc"; -import { Document } from "../../../../fields/documentSchemas"; -import { InkField } from "../../../../fields/InkField"; -import { BoolCast, Cast, NumCast } from "../../../../fields/Types"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SelectionManager } from "../../../util/SelectionManager"; -import AntimodeMenu, { AntimodeMenuProps } from "../../AntimodeMenu"; -import "./FormatShapePane.scss"; -import { undoBatch } from "../../../util/UndoManager"; -import { ColorState, SketchPicker } from 'react-color'; - -@observer -export default class FormatShapePane extends AntimodeMenu<AntimodeMenuProps> { - static Instance: FormatShapePane; - - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash = "2"; - private _mode = ["fill-drip", "ruler-combined"]; - - @observable private _subOpen = [false, false]; - @observable private _currMode = "fill-drip"; - @observable _lock = false; - @observable private _fillBtn = false; - @observable private _lineBtn = false; - @observable _controlBtn = false; - @observable private _controlPoints: { X: number, Y: number }[] = []; - @observable _currPoint = -1; - - getField(key: string) { - return this.selectedInk?.reduce((p, i) => - (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); - } - - @computed get selectedInk() { - const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); - return inks.length ? inks : undefined; - } - @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - set solidFil(value) { this.unFilled = !value; } - set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - } - set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - set shapeWid(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - i.rootDoc._width = Number(value); - this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - }); - } - set shapeHgt(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldHeight = NumCast(i.rootDoc._height); - i.rootDoc._height = Number(value); - this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - }); - } - - constructor(props: Readonly<{}>) { - super(props); - FormatShapePane.Instance = this; - this._canFade = false; - this.Pinned = BoolCast(Doc.UserDoc()["menuFormatShape-pinned"]); - } - - @action - closePane = () => { - this.fadeOut(false); - this.Pinned = false; - } - - @action - upDownButtons = (dirs: string, field: string) => { - switch (field) { - case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; - // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; - case "Xps": this.selectedInk?.forEach(i => i.rootDoc.x = NumCast(i.rootDoc.x) + (dirs === "up" ? 10 : -10)); break; - case "Yps": this.selectedInk?.forEach(i => i.rootDoc.y = NumCast(i.rootDoc.y) + (dirs === "up" ? 10 : -10)); break; - case "stk": this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = NumCast(i.rootDoc.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; - case "wid": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - //redraw points - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); - i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - } - } - - @undoBatch - @action - addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - for (var j = 0; j < 4; j++) { - newPoints.push({ X: x, Y: y }); - - } - for (var i = spNum; i < ink.length; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - } - } - } - })); - } - - @undoBatch - @action - deletePoints = () => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && this._currPoint !== -1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink && ink.length > 4) { - const newPoints: { X: number, Y: number }[] = []; - - console.log(ink.length, this._currPoint, Math.floor((this._currPoint + 2) / 4)); - - const toRemove = Math.floor(((this._currPoint + 2) / 4)); - for (var i = 0; i < ink.length; i++) { - if (Math.floor((i + 2) / 4) !== toRemove) { - console.log(i, toRemove); - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - if (newPoints.length === 4) { - const newerPoints: { X: number, Y: number }[] = []; - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - doc.data = new InkField(newerPoints); - - } - } - } - } - })); - } - - @undoBatch - @action - rotate = (angle: number) => { - const _centerPoints: { X: number, Y: number }[] = []; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - _centerPoints.push({ X: left, Y: top }); - } - } - })); - - var index = 0; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - doc.rotation = Number(doc.rotation) + Number(angle); - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - const xs = newPoints.map(p => p.X); - const ys = newPoints.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - - doc._height = (bottom - top); - doc._width = (right - left); - } - index++; - } - })); - } - - @undoBatch - @action - control = (xDiff: number, yDiff: number, controlNum: number) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; - for (var i = 0; i < ink.length; i++) { - if (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) - || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) { - newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); - } - else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - const oldx = doc.x; - const oldy = doc.y; - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - doc.data = new InkField(newPoints); - const xs2 = newPoints.map(p => p.X); - const ys2 = newPoints.map(p => p.Y); - const left2 = Math.min(...xs2); - const top2 = Math.min(...ys2); - const right2 = Math.max(...xs2); - const bottom2 = Math.max(...ys2); - doc._height = (bottom2 - top2); - doc._width = (right2 - left2); - //if points move out of bounds - - doc.x = oldx - (left - left2); - doc.y = oldy - (top - top2); - - } - } - } - })); - } - - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - } - - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } - - - colorPicker(setter: (color: string) => {}, type: string) { - return <div className="btn-group-palette" key="colorpicker" style={{ width: 160, margin: 10 }}> - <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={type === "stk" ? this.colorStk : this.colorFil} /> - </div>; - } - inputBox = (key: string, value: any, setter: (val: string) => {}) => { - return <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value} - onChange={undoBatch(action((e) => setter(e.target.value)))} - autoFocus /> - <button className="antiMenu-Buttonup" key="up1" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down1" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}> - ˅ - </button> - </>; - } - - inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { - return <> - {title1} - <p style={{ marginTop: -20, right: 70, position: "absolute" }}>{title2}</p> - - <input style={{ color: "black", width: 40, position: "absolute", right: 130 }} - type="text" value={value} - onChange={e => setter(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} style={{ right: 110 }}> - ˄ - </button> - <button className="antiMenu-Buttonup" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: 12, right: 110 }}> - ˅ - </button> - {title2 === "" ? "" : <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value2} - onChange={e => setter2(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up3" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key2)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down3" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key2)))} style={{ marginTop: -8 }}> - ˅ - </button></>} - </>; - } - - - colorButton(value: string, setter: () => {}) { - return <> - <button className="antimodeMenu-button" key="color" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "relative", marginTop: -5 }}> - <div className="color-previewII" style={{ backgroundColor: value ?? "121212" }} /> - {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -23, position: "fixed" }}>☒</p> : ""} - </button> - </>; - } - - controlPointsButton() { - return <> - <button className="antimodeMenu-button" title="Edit points" key="bezier" onPointerDown={action(() => this._controlBtn = this._controlBtn ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._controlBtn ? "black" : "" }}> - <FontAwesomeIcon icon="bezier-curve" size="lg" /> - </button> - <button className="antimodeMenu-button" title="Lock ratio" key="ratio" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._lock ? "black" : "" }}> - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <button className="antimodeMenu-button" key="rotate" title="Rotate 90˚" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "relative", marginTop: 10, fontSize: 15 }}> - ⟲ - </button> - <br /> <br /> - </>; - } - - lockRatioButton() { - return <> - <button className="antimodeMenu-button" key="lock" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "absolute", right: 80, backgroundColor: this._lock ? "black" : "" }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <br /> <br /> - </>; - } - - rotate90Button() { - return <> - <button className="antimodeMenu-button" key="rot" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "absolute", right: 80, }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - ⟲ - - </button> - <br /> <br /> - </>; - } - @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } - @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } - - @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } - @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } - - @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); } - @computed get dashInput() { return this.inputBox("dsh", this.widthStk, (val: string) => this.widthStk = val); } - - @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } - @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } - @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } - - @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } - - @computed get controlPoints() { return this.controlPointsButton(); } - @computed get lockRatio() { return this.lockRatioButton(); } - @computed get rotate90() { return this.rotate90Button(); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } - - - @computed get propertyGroupItems() { - const fillCheck = <div key="fill" style={{ display: (this._subOpen[0] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - Fill: - {this.fillButton} - <div style={{ float: "left", width: 100 }} > - Stroke: - {this.lineButton} - </div> - - {this._fillBtn ? this.fillPicker : ""} - {this._lineBtn ? this.linePicker : ""} - {this._fillBtn || this._lineBtn ? "" : <br />} - {(this.solidStk || this.dashdStk) ? "Width" : ""} - {(this.solidStk || this.dashdStk) ? this.stkInput : ""} - - - {(this.solidStk || this.dashdStk) ? <input type="range" defaultValue={Number(this.widthStk)} min={1} max={100} onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> : (null)} - <br /> - {(this.solidStk || this.dashdStk) ? <> - <p style={{ position: "absolute", fontSize: 12 }}>Arrow Head</p> - <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} style={{ position: "absolute", right: 110, width: 20 }} /> - <p style={{ position: "absolute", fontSize: 12, right: 30 }}>Arrow End</p> - <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} style={{ position: "absolute", right: 0, width: 20 }} /> - <br /> - </> : ""} - Dash: <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.dashdStk === "2"} onChange={undoBatch(action(() => this.dashdStk = this.dashdStk === "2" ? "0" : "2"))} style={{ position: "absolute", right: 110, width: 20 }} /> - - - - </div>; - - - - const sizeCheck = - - <div key="sizeCheck" style={{ display: (this._subOpen[1] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - {this.controlPoints} - {this.hgtInput} - {this.XpsInput} - {this.rotInput} - - </div>; - - - const subMenus = this._currMode === "fill-drip" ? [`Appearance`, 'Transform'] : []; - const menuItems = this._currMode === "fill-drip" ? [fillCheck, sizeCheck] : []; - const indexOffset = 0; - - return <div className="antimodeMenu-sub" key="submenu" style={{ position: "absolute", width: "inherit", top: 60 }}> - {subMenus.map((subMenu, i) => - <div key={subMenu} style={{ width: "inherit" }}> - <button className="antimodeMenu-button" onPointerDown={action(() => this._subOpen[i + indexOffset] = !this._subOpen[i + indexOffset])} - style={{ backgroundColor: "121212", position: "relative", width: "inherit" }}> - {this._subOpen[i + indexOffset] ? "▼" : "▶︎"} - {subMenu} - </button> - {menuItems[i]} - </div>)} - </div>; - } - - @computed get closeBtn() { - return <button className="antimodeMenu-button" key="close" onPointerDown={action(() => this.closePane())} style={{ position: "absolute", right: 0 }}> - X - </button>; - } - - @computed get propertyGroupBtn() { - return <div className="antimodeMenu-button-tab" key="modes"> - {this._mode.map(mode => - <button className="antimodeMenu-button" key={mode} onPointerDown={action(() => this._currMode = mode)} - style={{ backgroundColor: this._currMode === mode ? "121212" : "", position: "relative", top: 30 }}> - <FontAwesomeIcon icon={mode as IconProp} size="lg" /> - </button>)} - </div>; - } - - render() { - return this.getElementVert([this.closeBtn, - this.propertyGroupItems]); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 2cfe0183c..fd4fa0c7e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -1,13 +1,12 @@ import React = require("react"); -import AntimodeMenu, { AntimodeMenuProps } from "../../AntimodeMenu"; -import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction } from "../../../../Utils"; -import { undoBatch } from "../../../util/UndoManager"; import { Tooltip } from "@material-ui/core"; +import { observer } from "mobx-react"; +import { unimplementedFunction } from "../../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; @observer -export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { +export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: MarqueeOptionsMenu; public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; @@ -16,6 +15,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; + public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; constructor(props: Readonly<{}>) { super(props); @@ -24,6 +24,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> } render() { + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 19, transform: 'translate(-2px, -2px)' }} />; const buttons = [ <Tooltip key="group" title={<><div className="dash-tooltip">Create a Collection</div></>} placement="bottom"> <button @@ -53,6 +54,13 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> <FontAwesomeIcon icon="font" size="lg" /> </button> </Tooltip>, + <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected view</div></>} placement="bottom"> + <button + className="antimodeMenu-button" + onPointerDown={this.pinWithView}> + <>{presPinWithViewIcon}</> + </button> + </Tooltip>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c0b19fcd2..7c64fd429 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,27 +1,31 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl } from "../../../../fields/util"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { GetEffectiveAcl } from "../../../../fields/util"; import { Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; +import { DocumentManager } from "../../../util/DocumentManager"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PreviewCursor } from "../../PreviewCursor"; +import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; -import { CollectionView } from "../CollectionView"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { CollectionView, CollectionViewType } from "../CollectionView"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { ContextMenuItem } from "../../ContextMenuItem"; +import { Id } from "../../../../fields/FieldSymbols"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { PresMovement } from "../../nodes/PresBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -30,7 +34,7 @@ interface MarqueeViewProps { selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - nudge: (x: number, y: number) => boolean; + nudge?: (x: number, y: number) => boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -75,7 +79,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); if (e.key === "?") { cm.setDefaultItem("?", (str: string) => this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false, title: "bing", UseCors: true }), "onRight")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _fitWidth: true, _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -115,30 +119,29 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque })(); e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { + // e.preventDefault(); + // navigator.clipboard.readText().then(text => { + // const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + // if (ns.length === 1 && text.startsWith("http")) { + // this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer + // } else { + // this.pasteTable(ns, x, y); + // } + // }); + // e.stopPropagation(); + e.preventDefault(); - navigator.clipboard.readText().then(text => { - const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); - if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer - } else { - this.pasteTable(ns, x, y); - } - }); + const slide = Doc.copyDragFactory(Doc.UserDoc().emptySlide as Doc)!; + slide.x = x; + slide.y = y; + FormattedTextBox.SelectOnLoad = slide[Id]; + this.props.addDocument(slide); + //setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(slide)!, false)); e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey) { - FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; - const tbox = Docs.Create.TextDocument("", { - _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), - title: "-typed text-" - }); - const template = FormattedTextBox.DefaultLayout; - if (template instanceof Doc) { - tbox._width = NumCast(template._width); - tbox.layoutKey = "layout_" + StrCast(template.title); - Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; - } - this.props.addLiveTextDocument(tbox); + FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.ChildLayoutString ? e.key : ""; + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); + this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0)); e.stopPropagation(); } } @@ -241,6 +244,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); document.removeEventListener("pointerdown", hideMarquee); + document.removeEventListener("wheel", hideMarquee); }; if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { MarqueeOptionsMenu.Instance.createCollection = this.collection; @@ -250,7 +254,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; document.addEventListener("pointerdown", hideMarquee); + document.addEventListener("wheel", hideMarquee); } else { this.hideMarquee(); } @@ -349,7 +355,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, isBackground?: boolean) => { + getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, _isBackground?: boolean) => { const newCollection = creator ? creator(selected, { title: "nested stack", }) : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = "nested freeform"; @@ -357,8 +363,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; - newCollection.isBackground = isBackground; - newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined; + newCollection._isBackground = _isBackground; + newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : _isBackground ? "cyan" : undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection.x = this.Bounds.left; @@ -381,6 +387,38 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @undoBatch @action + pinWithView = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const doc = this.props.Document; + const bounds = this.Bounds; + const selected = this.marqueeSelect(false); + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + if (!DocumentManager.Instance.getDocumentView(curPres)) { + CollectionDockingView.AddSplit(curPres, "right"); + } + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + const x = this.Bounds.left + this.Bounds.width / 2; + const y = this.Bounds.top + this.Bounds.height / 2; + const panelWidth: number = this.props.PanelWidth(); + const panelHeight: number = this.props.PanelHeight(); + const scale = Math.min(Number(panelWidth) / this.Bounds.width, Number(panelHeight) / this.Bounds.height); + pinDoc.presPinView = true; + pinDoc.presPinViewX = x; + pinDoc.presPinViewY = y; + pinDoc.presPinViewScale = scale; + } + } + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + @undoBatch @action collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -487,12 +525,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); selected.map(d => { this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.x = NumCast(d.x) - bounds.left; + d.y = NumCast(d.y) - bounds.top; d.page = -1; return d; }); - const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); + const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); const portal = Doc.MakeAlias(summary); Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected); Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations"); @@ -644,7 +682,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque marqueeSelect(selectBackgrounds: boolean = true) { const selRect = this.Bounds; const selection: Doc[] = []; - this.props.activeDocuments().filter(doc => !doc.isBackground && !doc.z).map(doc => { + this.props.activeDocuments().filter(doc => !doc._isBackground && !doc.z).map(doc => { const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); @@ -719,14 +757,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque </div>; } else { - //subtracted 250 for offset var str: string = ""; for (var i = 0; i < this._pointsX.length; i++) { - var x = 0; - x = this._pointsX[i] - 250; - str += x.toString(); + const pt = this.props.getContainerTransform().transformPoint(this._pointsX[i], this._pointsY[i]); + str += pt[0].toString(); str += ","; - str += this._pointsY[i].toString(); + str += pt[1].toString(); str += (" "); } @@ -747,7 +783,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque render() { return <div className="marqueeView" - style={{ overflow: StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} + style={{ overflow: !this.props.ContainingCollectionView && this.props.annotationsKey ? "visible" : StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} onDragOver={e => e.preventDefault()} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss deleted file mode 100644 index 535581f2e..000000000 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ /dev/null @@ -1,744 +0,0 @@ -.propertiesView { - - background-color: rgb(205, 205, 205); - height: 100%; - font-family: "Noto Sans"; - cursor: auto; - - overflow-x: hidden; - overflow-y: scroll; - - .propertiesView-title { - background-color: rgb(159, 159, 159); - text-align: center; - padding-top: 12px; - padding-bottom: 12px; - display: flex; - font-size: 18px; - font-weight: bold; - justify-content: center; - - .propertiesView-title-icon { - width: 20px; - height: 20px; - padding-left: 38px; - margin-top: -5px; - align-items: flex-end; - margin-left: auto; - margin-right: 10px; - - &:hover { - color: grey; - cursor: pointer; - } - - } - - } - - .propertiesView-name { - border-bottom: 1px solid black; - padding: 8.5px; - font-size: 12.5px; - - &:hover { - cursor: text; - } - } - - .propertiesView-settings { - border-bottom: 1px solid black; - //padding: 8.5px; - font-size: 12.5px; - font-weight: bold; - - .propertiesView-settings-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-settings-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-settings-content { - margin-left: 12px; - padding-bottom: 10px; - padding-top: 8px; - } - - } - - .propertiesView-sharing { - border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-sharing-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-sharing-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-sharing-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - - .propertiesView-acls-checkbox { - float: right; - height: 20px; - margin-top: -20px; - margin-right: -15; - - .propertiesView-acls-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; - } - } - - .change-buttons { - display: flex; - - button { - width: 5; - height: 5; - } - - input { - width: 100%; - } - } - } - } - - .propertiesView-appearance { - border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-appearance-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-appearance-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-appearance-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } - } - - .propertiesView-transform { - border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-transform-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-transform-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-transform-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } - } - - .notify-button { - padding: 2px; - width: 12px; - height: 12px; - background-color: black; - border-radius: 10px; - padding-left: 2px; - padding-right: 2px; - margin-top: 2px; - margin-left: 3px; - - .notify-button-icon { - width: 6px; - height: 6.5px; - margin-left: .5px; - } - - &:hover { - background-color: rgb(158, 158, 158); - cursor: pointer; - } - } - - .expansion-button-icon { - width: 11px; - height: 11px; - color: black; - margin-left: 27px; - - &:hover { - color: rgb(131, 131, 131); - cursor: pointer; - } - } - - .propertiesView-sharingTable { - - // whatever's commented out - add it back in when adding the buttons - - // border: 1.5px solid black; - border: 1px solid black; - padding: 5px; // remove when adding buttons - border-radius: 6px; // remove when adding buttons - margin-right: 10px; // remove when adding buttons - // width: 100%; - // display: inline-table; - background-color: #ececec; - max-height: 130px; - overflow-y: scroll; - width: 92%; - - .propertiesView-sharingTable-item { - - display: flex; - // padding: 5px; - padding: 3px; - align-items: center; - border-bottom: 0.5px solid grey; - - &:hover .propertiesView-sharingTable-item-name { - overflow-x: unset; - white-space: unset; - overflow-wrap: break-word; - } - - .propertiesView-sharingTable-item-name { - font-weight: bold; - width: 95px; - overflow-x: hidden; - display: inline-block; - text-overflow: ellipsis; - white-space: nowrap; - } - - .propertiesView-sharingTable-item-permission { - display: flex; - align-items: flex-end; - margin-left: auto; - - .permissions-select { - z-index: 1; - border: none; - background-color: inherit; - width: 75px; - //text-align: justify; // for Edge - //text-align-last: end; - - &:hover { - cursor: pointer; - } - } - } - - &:last-child { - border-bottom: none; - } - } - } - - .propertiesView-fields { - border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-fields-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-fields-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - - } - - .propertiesView-fields-checkbox { - float: right; - height: 20px; - margin-top: -9px; - - .propertiesView-fields-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; - } - } - - .propertiesView-fields-content { - font-size: 10px; - margin-left: 2px; - padding: 10px; - - &:hover { - cursor: pointer; - } - } - } - - .field { - display: flex; - font-size: 7px; - background-color: #e8e8e8; - padding-right: 3px; - border: 0.5px solid grey; - border-radius: 5px; - padding-left: 3px; - } - - .uneditable-field { - display: flex; - overflow-y: visible; - margin-bottom: 2px; - - &:hover { - cursor: auto; - } - } - - .propertiesView-layout { - - .propertiesView-layout-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-layout-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-layout-content { - overflow: hidden; - padding: 10px; - } - - } - - .propertiesView-presTrails { - border-bottom: 1px solid black; - //padding: 8.5px; - - .propertiesView-presTrails-title { - font-weight: bold; - font-size: 12.5px; - padding: 4px; - display: flex; - color: white; - padding-left: 8px; - background-color: rgb(51, 51, 51); - - &:hover { - cursor: pointer; - } - - .propertiesView-presTrails-title-icon { - float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; - - &:hover { - cursor: pointer; - } - } - } - - .propertiesView-presTrails-content { - font-size: 10px; - padding: 10px; - margin-left: 5px; - } - } -} - -.inking-button { - - display: flex; - - .inking-button-points { - background-color: #333333; - padding: 7px; - border-radius: 7px; - margin-right: 32px; - width: 32; - height: 32; - padding-top: 9px; - margin-left: 18px; - - &:hover { - background: rgb(131, 131, 131); - transform: scale(1.05); - cursor: pointer; - } - } - - .inking-button-lock { - background-color: #333333; - padding: 7px; - border-radius: 7px; - margin-right: 32px; - width: 32; - height: 32; - padding-top: 9px; - padding-left: 10px; - - &:hover { - background: rgb(131, 131, 131); - transform: scale(1.05); - cursor: pointer; - } - } - - .inking-button-rotate { - background-color: #333333; - padding: 7px; - border-radius: 7px; - width: 32; - height: 32; - padding-top: 9px; - padding-left: 10px; - - &:hover { - background: rgb(131, 131, 131); - transform: scale(1.05); - cursor: pointer; - } - } -} - -.inputBox-duo { - display: flex; -} - -.inputBox { - - margin-top: 10px; - display: flex; - height: 19px; - margin-right: 15px; - - .inputBox-title { - font-size: 12px; - padding-right: 5px; - } - - .inputBox-input { - font-size: 10px; - width: 50px; - margin-right: 1px; - border-radius: 3px; - - &:hover { - cursor: pointer; - } - } - - .inputBox-button { - - .inputBox-button-up { - background-color: #333333; - height: 9px; - padding-left: 3px; - padding-right: 3px; - padding-top: 1px; - padding-bottom: 1px; - border-radius: 1.5px; - - &:hover { - background: rgb(131, 131, 131); - transform: scale(1.05); - cursor: pointer; - } - } - - .inputBox-button-down { - background-color: #333333; - height: 9px; - padding-left: 3px; - padding-right: 3px; - padding-top: 1px; - padding-bottom: 1px; - border-radius: 1.5px; - - &:hover { - background: rgb(131, 131, 131); - transform: scale(1.05); - cursor: pointer; - } - } - - } -} - -.color-palette { - width: 160px; - height: 360; -} - -.strokeAndFill { - display: flex; - margin-top: 10px; - - .fill { - margin-right: 40px; - display: flex; - padding-bottom: 7px; - margin-left: 35px; - - .fill-title { - font-size: 12px; - margin-right: 2px; - } - - .fill-button { - padding-top: 2px; - margin-top: -1px; - } - } - - .stroke { - display: flex; - - .stroke-title { - font-size: 12px; - } - - .stroke-button { - padding-top: 2px; - margin-left: 2px; - margin-top: -1px; - } - } -} - -.propertiesView-presSelected { - border-top: solid 1px darkgrey; - width: 100%; - padding-top: 5px; - font-family: Roboto; - font-weight: 500; - display: inline-flex; - - .propertiesView-selectedList { - border-left: solid 1px darkgrey; - margin-left: 10px; - padding-left: 5px; - - .selectedList-items { - font-size: 12; - font-weight: 300; - margin-top: 1; - } - } -} - -.widthAndDash { - - .width { - .width-top { - display: flex; - - .width-title { - font-size: 12; - margin-right: 20px; - margin-left: 35px; - text-align: center; - } - - .width-input { - margin-right: 30px; - margin-top: -10px; - } - } - - .width-range { - margin-right: 1px; - margin-bottom: 6; - } - } - - .arrows { - - display: flex; - margin-bottom: 3px; - margin-left: 4px; - - .arrows-head { - - display: flex; - margin-right: 35px; - - .arrows-head-title { - font-size: 10; - } - - .arrows-head-input { - margin-left: 6px; - margin-top: 2px; - } - } - - .arrows-tail { - display: flex; - - .arrows-tail-title { - font-size: 10; - } - - .arrows-tail-input { - margin-left: 6px; - margin-top: 2px; - } - } - } - - .dashed { - - display: flex; - margin-left: 64px; - margin-bottom: 6px; - - .dashed-title { - font-size: 10; - } - - .dashed-input { - margin-left: 6px; - margin-top: 2px; - } - } -} - -.editable-title { - border: none; - padding: 6px; - padding-bottom: 2px; - - - &:hover { - border: 0.75px solid rgb(122, 28, 28); - } -} - - -.properties-flyout { - grid-column: 2/4; -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx deleted file mode 100644 index ed451beab..000000000 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ /dev/null @@ -1,1060 +0,0 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import "./PropertiesView.scss"; -import { observable, action, computed, runInAction } from "mobx"; -import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync, DataSym } from "../../../../fields/Doc"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { EditableView } from "../../EditableView"; -import { KeyValueBox } from "../../nodes/KeyValueBox"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; -import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; -import { Id } from "../../../../fields/FieldSymbols"; -import { Transform } from "../../../util/Transform"; -import { PropertiesButtons } from "../../PropertiesButtons"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip, Checkbox } from "@material-ui/core"; -import SharingManager from "../../../util/SharingManager"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; -import { InkField } from "../../../../fields/InkField"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; -import { ColorState, SketchPicker } from "react-color"; -import "./FormatShapePane.scss"; -import { PresBox } from "../../nodes/PresBox"; -import { DocumentManager } from "../../../util/DocumentManager"; -import FormatShapePane from "./FormatShapePane"; -const higflyout = require("@hig/flyout"); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; -const _global = (window /* browser */ || global /* node */) as any; - -// import * as fa from '@fortawesome/free-solid-svg-icons'; -// import { library } from "@fortawesome/fontawesome-svg-core"; - -// library.add(fa.faPlus, fa.faMinus, fa.faCog); - -interface PropertiesViewProps { - width: number; - height: number; - renderDepth: number; - ScreenToLocalTransform: () => Transform; - onDown: (event: any) => void; -} - -@observer -export class PropertiesView extends React.Component<PropertiesViewProps> { - private _widthUndo?: UndoManager.Batch; - - @computed get MAX_EMBED_HEIGHT() { return 200; } - - @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else if (PresBox.Instance && PresBox.Instance._selectedArray.length) { - return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); - } else { return undefined; } - } - @computed get isPres(): boolean { - if (this.selectedDoc?.type === DocumentType.PRES) return true; - return false; - } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } - - @observable layoutFields: boolean = false; - - @observable openActions: boolean = true; - @observable openSharing: boolean = true; - @observable openFields: boolean = true; - @observable openLayout: boolean = true; - @observable openAppearance: boolean = true; - @observable openTransform: boolean = true; - // @observable selectedUser: string = ""; - // @observable addButtonPressed: boolean = false; - @observable layoutDocAcls: boolean = false; - - //Pres Trails booleans: - @observable openPresTransitions: boolean = false; - @observable openPresProgressivize: boolean = false; - @observable openAddSlide: boolean = false; - @observable openSlideOptions: boolean = false; - - @observable inActions: boolean = false; - @observable _controlBtn: boolean = false; - @observable _lock: boolean = false; - - @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } - - @action - rtfWidth = () => { - if (this.selectedDoc) { - return Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); - } else { - return 0; - } - } - @action - rtfHeight = () => { - if (this.selectedDoc) { - return this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - } else { - return 0; - } - } - - @action - docWidth = () => { - if (this.selectedDoc) { - const layoutDoc = this.selectedDoc; - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.width - 20)); - return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; - } else { - return 0; - } - } - - @action - docHeight = () => { - if (this.selectedDoc && this.dataDoc) { - const layoutDoc = this.selectedDoc; - return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return this.docWidth() * aspect; - return layoutDoc._fitWidth ? (!this.dataDoc._nativeHeight ? NumCast(this.props.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, - NumCast(this.props.height)))) : - NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; - })())); - } else { - return 0; - } - } - - @computed get expandedField() { - if (this.dataDoc && this.selectedDoc) { - const ids: { [key: string]: string } = {}; - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); - const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - const contents = doc[key]; - if (key[0] === "#") { - rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); - } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} - />; - rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} - </div>); - } - } - rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> - <EditableView - key="editableView" - contents={"add key:value or #tags"} - height={13} - fontSize={10} - GetValue={() => ""} - SetValue={this.setKeyValue} /> - </div>); - return rows; - } - } - - @computed get noviceFields() { - if (this.dataDoc && this.selectedDoc) { - const ids: { [key: string]: string } = {}; - const doc = this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); - const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL" && key !== "UseCors") - || key[0] === "#" || key === "author" || - key === "creationDate" || key.indexOf("lastModified") !== -1) { - - const contents = doc[key]; - if (key[0] === "#") { - rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); - } else { - const value = Field.toString(contents as Field); - if (key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { - rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> - <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> - </div>); - } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} - />; - - rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} - </div>); - } - } - } - } - rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> - <EditableView - key="editableView" - contents={"add key:value or #tags"} - height={13} - fontSize={10} - GetValue={() => ""} - SetValue={this.setKeyValue} /> - </div>); - return rows; - } - } - - @undoBatch - setKeyValue = (value: string) => { - if (this.selectedDoc && this.dataDoc) { - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; - if (value.indexOf(":") !== -1) { - const newVal = value[0].toUpperCase() + value.substring(1, value.length); - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); - return true; - } else if (value[0] === "#") { - const newVal = value + `:'${value}'`; - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); - return true; - } - } - return false; - } - - @observable transform: Transform = Transform.Identity(); - getTransform = () => this.transform; - propertiesDocViewRef = (ref: HTMLDivElement) => { - const observer = new _global.ResizeObserver(action((entries: any) => { - const cliRect = ref.getBoundingClientRect(); - this.transform = new Transform(-cliRect.x, -cliRect.y, 1); - })); - ref && observer.observe(ref); - } - - previewBackground = () => "lightgrey"; - @computed get layoutPreview() { - if (this.selectedDoc) { - const layoutDoc = Doc.Layout(this.selectedDoc); - const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; - const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; - return <div ref={this.propertiesDocViewRef} style={{ pointerEvents: "none", display: "inline-block", height: panelHeight() }} key={this.selectedDoc[Id]}> - <ContentFittingDocumentView - Document={layoutDoc} - DataDoc={this.dataDoc} - LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} - rootSelected={returnFalse} - treeViewDoc={undefined} - backgroundColor={this.previewBackground} - fitToBox={true} - FreezeDimensions={true} - NativeWidth={layoutDoc.type === - StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : returnZero} - NativeHeight={layoutDoc.type === - StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : returnZero} - PanelWidth={panelWidth} - PanelHeight={panelHeight} - focus={returnFalse} - ScreenToLocalTransform={this.getTransform} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - addDocument={returnFalse} - moveDocument={undefined} - removeDocument={returnFalse} - parentActive={() => false} - whenActiveChanged={emptyFunction} - addDocTab={returnFalse} - pinToPres={emptyFunction} - bringToFront={returnFalse} - ContentScaling={returnOne} - dontRegisterView={true} - dropAction={undefined} - /> - </div>; - } else { - return null; - } - } - - /** - * Handles the changing of a user's permissions from the permissions panel. - */ - @undoBatch - changePermissions = (e: any, user: string) => { - SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); - } - - /** - * @returns the options for the permissions dropdown. - */ - getPermissionsSelect(user: string, permission: string) { - return <select className="permissions-select" - value={permission} - onChange={e => this.changePermissions(e, user)}> - {Object.values(SharingPermissions).map(permission => { - return ( - <option key={permission} value={permission} selected={this.selectedDoc![`ACL-${user.replace(".", "_")}`] === permission}> - {permission} - </option>); - })} - </select>; - } - - /** - * @returns the notification icon. On clicking, it should notify someone of a document been shared with them. - */ - @computed get notifyIcon() { - return <Tooltip title={<div className="dash-tooltip">Notify with message</div>}> - <div className="notify-button"> - <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" /> - </div> - </Tooltip>; - } - - /** - * ... next to the owner that opens the main SharingManager interface on click. - */ - @computed get expansionIcon() { - return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> - <div className="expansion-button" onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); - } - }}> - <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> - </div> - </Tooltip>; - } - - /** - * @returns a row of the permissions panel - */ - sharingItem(name: string, effectiveAcl: symbol, permission: string) { - return <div className="propertiesView-sharingTable-item" key={name + permission} - // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }} - // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)} - > - <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> - {/* {name !== "Me" ? this.notifyIcon : null} */} - <div className="propertiesView-sharingTable-item-permission"> - {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} - {permission === "Owner" ? this.expansionIcon : null} - </div> - </div>; - } - - /** - * @returns the sharing and permissiosn panel. - */ - @computed get sharingTable() { - const AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin] - ]); - - const target = this.layoutDocAcls ? this.selectedDoc! : this.selectedDoc![DataSym]; - - const effectiveAcl = GetEffectiveAcl(target); - const tableEntries = []; - - // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (target[AclSym]) { - for (const [key, value] of Object.entries(target[AclSym])) { - const name = key.substring(4).replace("_", "."); - if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public"/* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value as symbol)!)); - } - } - } - - // if (Doc.UserDoc().sidebarUsersDisplayed) { - // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) { - // if (value === true && !this.selectedDoc![`ACL-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None)); - // } - // } - // }) - - // shifts the current user, owner, public to the top of the doc. - tableEntries.unshift(this.sharingItem("Public", effectiveAcl, (AclMap.get(target[AclSym]?.["ACL-Public"]) || SharingPermissions.None))); - tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === target.author ? "Owner" : AclMap.get(effectiveAcl)!)); - if (Doc.CurrentUserEmail !== target.author) tableEntries.unshift(this.sharingItem(StrCast(target.author), effectiveAcl, "Owner")); - - return <div className="propertiesView-sharingTable"> - {tableEntries} - </div>; - } - - @computed get fieldsCheckbox() { - return <Checkbox - color="primary" - onChange={this.toggleCheckbox} - checked={this.layoutFields} - />; - } - - @action - toggleCheckbox = () => { - this.layoutFields = !this.layoutFields; - } - - @computed get editableTitle() { - return <div className="editable-title"><EditableView - key="editableView" - contents={StrCast(this.selectedDoc?.title)} - height={25} - fontSize={14} - GetValue={() => StrCast(this.selectedDoc?.title)} - SetValue={this.setTitle} /> </div>; - } - - @undoBatch - @action - setTitle = (value: string) => { - if (this.dataDoc) { - this.selectedDoc && (this.selectedDoc.title = value); - KeyValueBox.SetField(this.dataDoc, "title", value, true); - return true; - } - return false; - } - - - @undoBatch - @action - rotate = (angle: number) => { - const _centerPoints: { X: number, Y: number }[] = []; - if (this.selectedDoc) { - const doc = this.selectedDoc; - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - _centerPoints.push({ X: left, Y: top }); - } - } - - var index = 0; - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - doc.rotation = Number(doc.rotation) + Number(angle); - const inks = Cast(doc.data, InkField)?.inkData; - if (inks) { - const newPoints: { X: number, Y: number }[] = []; - inks.forEach(ink => { - const newX = Math.cos(angle) * (ink.X - _centerPoints[index].X) - Math.sin(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (ink.X - _centerPoints[index].X) + Math.cos(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].Y; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - const xs = newPoints.map(p => p.X); - const ys = newPoints.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - - doc._height = (bottom - top); - doc._width = (right - left); - } - index++; - } - } - } - - - - @computed - get controlPointsButton() { - return <div className="inking-button"> - <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> - <div className="inking-button-points" onPointerDown={action(() => FormatShapePane.Instance._controlBtn = !FormatShapePane.Instance._controlBtn)} style={{ backgroundColor: FormatShapePane.Instance._controlBtn ? "black" : "" }}> - <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{FormatShapePane.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> - <div className="inking-button-lock" onPointerDown={action(() => FormatShapePane.Instance._lock = !FormatShapePane.Instance._lock)} > - <FontAwesomeIcon icon={FormatShapePane.Instance._lock ? "lock" : "unlock"} color="white" size="lg" /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}> - <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}> - <FontAwesomeIcon icon="undo" color="white" size="lg" /> - </div> - </Tooltip> - </div>; - } - - inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => { - return <div className="inputBox" - style={{ - marginRight: title === "X:" ? "19px" : "", - marginLeft: title === "∠:" ? "39px" : "" - }}> - <div className="inputBox-title"> {title} </div> - <input className="inputBox-input" - type="text" value={value} - onChange={e => setter(e.target.value)} /> - <div className="inputBox-button"> - <div className="inputBox-button-up" key="up2" - onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > - <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> - </div> - <div className="inputbox-Button-down" key="down2" - onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > - <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> - </div> - </div> - </div>; - } - - inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { - return <div className="inputBox-duo"> - {this.inputBox(key, value, setter, title1)} - {title2 === "" ? (null) : this.inputBox(key2, value2, setter2, title2)} - </div>; - } - - @action - upDownButtons = (dirs: string, field: string) => { - switch (field) { - case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; - // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; - case "Xps": this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === "up" ? 10 : -10)); break; - case "Yps": this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === "up" ? 10 : -10)); break; - case "stk": this.selectedDoc && (this.selectedDoc.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; - case "wid": - const oldWidth = NumCast(this.selectedDoc?._width); - const oldHeight = NumCast(this.selectedDoc?._height); - const oldX = NumCast(this.selectedDoc?.x); - const oldY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); - const doc = this.selectedDoc; - if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - for (var j = 0; j < ink.length; j++) { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = (NumCast(doc.x) - oldX) + (ink[j].X * NumCast(doc._width)) / oldWidth; - const newY = (NumCast(doc.y) - oldY) + (ink[j].Y * NumCast(doc._height)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - } - doc.data = new InkField(newPoints); - } - } - break; - case "hgt": - const oWidth = NumCast(this.selectedDoc?._width); - const oHeight = NumCast(this.selectedDoc?._height); - const oX = NumCast(this.selectedDoc?.x); - const oY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); - const docu = this.selectedDoc; - if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { - const ink = Cast(docu.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - for (var j = 0; j < ink.length; j++) { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = (NumCast(docu.x) - oX) + (ink[j].X * NumCast(docu._width)) / oWidth; - const newY = (NumCast(docu.y) - oY) + (ink[j].Y * NumCast(docu._height)) / oHeight; - newPoints.push({ X: newX, Y: newY }); - } - docu.data = new InkField(newPoints); - } - } - break; - } - } - - getField(key: string) { - //if (this.selectedDoc) { - return Field.toString(this.selectedDoc?.[key] as Field); - // } else { - // return undefined as Opt<string>; - // } - } - - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Number(value)); } - set shapeYps(value) { this.selectedDoc && (this.selectedDoc.y = Number(value)); } - set shapeRot(value) { this.selectedDoc && (this.selectedDoc.rotation = Number(value)); } - set shapeWid(value) { - const oldWidth = NumCast(this.selectedDoc?._width); - this.selectedDoc && (this.selectedDoc._width = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); - } - set shapeHgt(value) { - const oldHeight = NumCast(this.selectedDoc?._height); - this.selectedDoc && (this.selectedDoc._height = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); - } - - @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => { if (val !== "0" && !isNaN(Number(val))) { this.shapeXps = val; } return true; }, "X:", "Yps", this.shapeYps, (val: string) => { if (val !== "0" && !isNaN(Number(val))) { this.shapeYps = val; } return true; }, "Y:"); } - @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, "∠:", "rot", this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, ""); } - - - @observable private _fillBtn = false; - @observable private _lineBtn = false; - - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash: any = "2"; - - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - set colorFil(value) { value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } - - colorButton(value: string, type: string, setter: () => {}) { - // return <div className="properties-flyout" onPointerEnter={e => this.changeScrolling(false)} - // onPointerLeave={e => this.changeScrolling(true)}> - // <Flyout anchorPoint={anchorPoints.LEFT_TOP} - // content={type === "fill" ? this.fillPicker : this.linePicker}> - return <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> - <div className="color-button-preview" style={{ - backgroundColor: value ?? "121212", width: 15, height: 15, - display: value === "" || value === "transparent" ? "none" : "" - }} /> - {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -14 }}>☒</p> : ""} - </div>; - // </Flyout> - // </div>; - - } - - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - } - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } - - colorPicker(setter: (color: string) => {}, type: string) { - return <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} - presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', - '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', - '#FFFFFF', '#f1efeb', 'transparent']} - color={type === "stk" ? this.colorStk : this.colorFil} />; - } - - @computed get fillButton() { return this.colorButton(this.colorFil, "fill", () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } - @computed get lineButton() { return this.colorButton(this.colorStk, "line", () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } - - @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } - @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } - - @computed get strokeAndFill() { - return <div> - <div key="fill" className="strokeAndFill"> - <div className="fill"> - <div className="fill-title">Fill:</div> - <div className="fill-button">{this.fillButton}</div> - </div> - <div className="stroke"> - <div className="stroke-title"> Stroke: </div> - <div className="stroke-button">{this.lineButton}</div> - </div> - </div> - {this._fillBtn ? this.fillPicker : ""} - {this._lineBtn ? this.linePicker : ""} - </div>; - } - - @computed get solidStk() { return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === "0") ? true : false; } - @computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; } - @computed get unStrokd() { return this.selectedDoc?.color ? true : false; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); - } - set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedDoc && (this.selectedDoc.strokeEndMarker = value); } - - - @computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); } - - - regInput = (key: string, value: any, setter: (val: string) => {}) => { - return <div className="inputBox"> - <input className="inputBox-input" - type="text" value={value} - onChange={e => setter(e.target.value)} /> - <div className="inputBox-button"> - <div className="inputBox-button-up" key="up2" - onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > - <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> - </div> - <div className="inputbox-Button-down" key="down2" - onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > - <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> - </div> - </div> - </div>; - } - - @computed get widthAndDash() { - return <div className="widthAndDash"> - <div className="width"> - <div className="width-top"> - <div className="width-title">Width:</div> - <div className="width-input">{this.stkInput}</div> - </div> - <input className="width-range" type="range" - defaultValue={Number(this.widthStk)} min={1} max={100} - onChange={(action((e) => this.widthStk = e.target.value))} - onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("width undo"); }} - onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }} - /> - </div> - - <div className="arrows"> - <div className="arrows-head"> - <div className="arrows-head-title" >Arrow Head: </div> - <input key="markHead" className="arrows-head-input" type="checkbox" - checked={this.markHead !== ""} - onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} /> - </div> - <div className="arrows-tail"> - <div className="arrows-tail-title" >Arrow End: </div> - <input key="markTail" className="arrows-tail-input" type="checkbox" - checked={this.markTail !== ""} - onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} /> - </div> - </div> - <div className="dashed"> - <div className="dashed-title">Dashed Line:</div> - <input key="markHead" className="dashed-input" - type="checkbox" checked={this.dashdStk === "2"} - onChange={this.changeDash} /> - </div> - </div>; - } - - @undoBatch @action - changeDash = () => { - this.dashdStk = this.dashdStk === "2" ? "0" : "2"; - } - - @computed get appearanceEditor() { - return <div className="appearance-editor"> - {this.widthAndDash} - {this.strokeAndFill} - </div>; - } - - @computed get transformEditor() { - return <div className="transform-editor"> - {this.controlPointsButton} - {this.hgtInput} - {this.XpsInput} - {this.rotInput} - </div>; - } - - /** - * Handles adding and removing members from the sharing panel - */ - // handleUserChange = (selectedUser: string, add: boolean) => { - // if (!Doc.UserDoc().sidebarUsersDisplayed) Doc.UserDoc().sidebarUsersDisplayed = new Doc; - // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - // sidebarUsersDisplayed![`display-${selectedUser}`] = add; - // !add && runInAction(() => this.selectedUser = ""); - // }); - // } - - render() { - if (!this.selectedDoc && !this.isPres) { - return <div className="propertiesView" style={{ width: this.props.width }}> - <div className="propertiesView-title" style={{ width: this.props.width }}> - No Document Selected - </div> - </div>; - - } else { - const novice = Doc.UserDoc().noviceMode; - - if (this.selectedDoc && !this.isPres) { - return <div className="propertiesView" style={{ - width: this.props.width, - //overflowY: this.scrolling ? "scroll" : "visible" - }} > - <div className="propertiesView-title" style={{ width: this.props.width }}> - Properties - {/* <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> - <FontAwesomeIcon icon="times" color="black" size="sm" /> - </div> */} - </div> - <div className="propertiesView-name"> - {this.editableTitle} - </div> - <div className="propertiesView-settings" onPointerEnter={() => runInAction(() => { this.inActions = true; })} - onPointerLeave={action(() => this.inActions = false)}> - <div className="propertiesView-settings-title" - onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} - style={{ backgroundColor: this.openActions ? "black" : "" }}> - Actions - <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openActions ? (null) : - <div className="propertiesView-settings-content"> - <PropertiesButtons /> - </div>} - </div> - <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openSharing ? (null) : - <div className="propertiesView-sharing-content"> - <div className="propertiesView-acls-checkbox"> - <Checkbox - color="primary" - onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} - checked={this.layoutDocAcls} - />; - <div className="propertiesView-acls-checkbox-text">Layout</div> - </div> - {this.sharingTable} - {/* <div className="change-buttons"> - <button - onPointerDown={action(() => this.addButtonPressed = !this.addButtonPressed)} - > - <FontAwesomeIcon icon={fa.faPlus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /> - </button> - <button - id="sharingProperties-removeUser" - onPointerDown={() => this.handleUserChange(this.selectedUser, false)} - style={{ backgroundColor: this.selectedUser ? "#121721" : "#777777" }} - ><FontAwesomeIcon icon={fa.faMinus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> - <button onClick={() => SharingManager.Instance.open(this.selectedDocumentView!)}><FontAwesomeIcon icon={fa.faCog} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> - {this.addButtonPressed ? - // <input type="text" onKeyDown={this.handleKeyPress} /> : - <select onChange={e => this.handleUserChange(e.target.value, true)}> - <option selected disabled hidden> - Add users - </option> - {SharingManager.Instance.users.map(user => - (<option value={user.user.email}> - {user.user.email} - </option>) - )} - {GroupManager.Instance.getAllGroups().map(group => - (<option value={StrCast(group.groupName)}> - {StrCast(group.groupName)} - </option>))} - </select> : - null} - </div> */} - </div>} - </div> - - {!this.isInk ? (null) : - <div className="propertiesView-appearance"> - <div className="propertiesView-appearance-title" - onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} - style={{ backgroundColor: this.openAppearance ? "black" : "" }}> - Appearance - <div className="propertiesView-appearance-title-icon"> - <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openAppearance ? (null) : - <div className="propertiesView-appearance-content"> - {this.appearanceEditor} - </div>} - </div>} - - {this.isInk ? <div className="propertiesView-transform"> - <div className="propertiesView-transform-title" - onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} - style={{ backgroundColor: this.openTransform ? "black" : "" }}> - Transform - <div className="propertiesView-transform-title-icon"> - <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openTransform ? <div className="propertiesView-transform-content"> - {this.transformEditor} - </div> : null} - </div> : null} - - <div className="propertiesView-fields"> - <div className="propertiesView-fields-title" - onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} - style={{ backgroundColor: this.openFields ? "black" : "" }}> - Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> - <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> - {this.fieldsCheckbox} - <div className="propertiesView-fields-checkbox-text">Layout</div> - </div> : null} - {!this.openFields ? (null) : - <div className="propertiesView-fields-content"> - {novice ? this.noviceFields : this.expandedField} - </div>} - </div> - <div className="propertiesView-layout"> - <div className="propertiesView-layout-title" - onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} - style={{ backgroundColor: this.openLayout ? "black" : "" }}> - Layout - <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> - <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} - </div> - </div>; - } - if (this.isPres) { - const selectedItem: boolean = PresBox.Instance?._selectedArray.length > 0; - return <div className="propertiesView" style={{ width: this.props.width }}> - <div className="propertiesView-title" style={{ width: this.props.width }}> - Presentation - </div> - <div className="propertiesView-name"> - {this.editableTitle} - <div className="propertiesView-presSelected"> - {PresBox.Instance?._selectedArray.length} selected - <div className="propertiesView-selectedList"> - {PresBox.Instance?.listOfSelected} - </div> - </div> - </div> - {!selectedItem ? (null) : <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={action(() => { this.openPresTransitions = !this.openPresTransitions; })} - style={{ backgroundColor: this.openPresTransitions ? "black" : "" }}> - <FontAwesomeIcon icon={"rocket"} /> Transitions - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresTransitions ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openPresTransitions ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.transitionDropdown} - </div> : null} - </div>} - {!selectedItem ? (null) : <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openPresProgressivize = !this.openPresProgressivize; })} - style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> - <FontAwesomeIcon icon={"tasks"} /> Progressivize - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresProgressivize ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openPresProgressivize ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.progressivizeDropdown} - </div> : null} - </div>} - {!selectedItem ? (null) : <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openSlideOptions = !this.openSlideOptions; })} - style={{ backgroundColor: this.openSlideOptions ? "black" : "" }}> - <FontAwesomeIcon icon={"cog"} /> {PresBox.Instance.stringType} options - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openSlideOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openSlideOptions ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.optionsDropdown} - </div> : null} - </div>} - <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openAddSlide = !this.openAddSlide; })} - style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> - <FontAwesomeIcon icon={"plus"} /> Add new slide - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openAddSlide ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openAddSlide ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.newDocumentDropdown} - </div> : null} - </div> - {/* <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openSharing ? <div className="propertiesView-sharing-content"> - {this.sharingTable} - </div> : null} - </div> */} - </div>; - } - } - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 4d8473be9..a6171af51 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -13,13 +13,6 @@ display: flex; flex-direction: row; - .imageBox-cont img { - height: auto; - width: auto; - max-height: 100%; - max-width: 100%; - } - .react-grid-layout { width: 100%; } @@ -134,17 +127,6 @@ } -// .documentDecorations-container .documentDecorations-resizer { -// pointer-events: none; -// } - -// #documentDecorations-bottomRightResizer, -// #documentDecorations-bottomLeftResizer, -// #documentDecorations-topRightResizer, -// #documentDecorations-topLeftResizer { -// visibility: collapse; -// } - /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e6ac7021a..d31427829 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { makeInterface } from '../../../../fields/Schema'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, OmitKeys } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -94,8 +94,8 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ unflexedPosition(index: number): Omit<Layout, "i"> { return { - x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW, - y: Math.floor(index / Math.floor(this.numCols / this.defaultH)) * this.defaultH, + x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, + y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, static: true @@ -162,11 +162,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { return <ContentFittingDocumentView - {...this.props} + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} Document={layout} DataDoc={layout.resolvedDataDoc as Doc} - NativeHeight={returnZero} - NativeWidth={returnZero} backgroundColor={this.props.backgroundColor} ContainingCollectionDoc={this.props.Document} PanelWidth={width} @@ -175,7 +173,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { onClick={this.onChildClickHandler} renderDepth={this.props.renderDepth + 1} parentActive={this.props.active} - display={StrCast(this.props.Document.display, "contents")} // sets the css display type of the ContentFittingDocumentView component + dontCenter={true} />; } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 21d283547..b99bef15e 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -213,9 +213,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return this.props.addDocTab(doc, where); } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + const layoutTemp = this.props.DataDoc ? true : undefined; return <ContentFittingDocumentView Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} + DataDoc={layout.resolvedDataDoc as Doc || (layoutTemp ? layout : undefined)} backgroundColor={this.props.backgroundColor} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -224,8 +225,6 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={false} rootSelected={this.rootSelected} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} @@ -234,6 +233,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index d02088a6c..c5e1816d9 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -224,8 +224,6 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={false} rootSelected={this.rootSelected} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} @@ -234,6 +232,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} |
